@doclo/core 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +34 -0
- package/dist/index.d.ts +931 -0
- package/dist/index.js +2293 -0
- package/dist/index.js.map +1 -0
- package/dist/internal/validation-utils.d.ts +1 -0
- package/dist/internal/validation-utils.js +650 -0
- package/dist/internal/validation-utils.js.map +1 -0
- package/dist/observability/index.d.ts +933 -0
- package/dist/observability/index.js +630 -0
- package/dist/observability/index.js.map +1 -0
- package/dist/pdf-utils.d.ts +123 -0
- package/dist/pdf-utils.js +106 -0
- package/dist/pdf-utils.js.map +1 -0
- package/dist/runtime/base64.d.ts +100 -0
- package/dist/runtime/base64.js +52 -0
- package/dist/runtime/base64.js.map +1 -0
- package/dist/runtime/crypto.d.ts +56 -0
- package/dist/runtime/crypto.js +35 -0
- package/dist/runtime/crypto.js.map +1 -0
- package/dist/runtime/env.d.ts +130 -0
- package/dist/runtime/env.js +76 -0
- package/dist/runtime/env.js.map +1 -0
- package/dist/security/index.d.ts +236 -0
- package/dist/security/index.js +260 -0
- package/dist/security/index.js.map +1 -0
- package/dist/validation-CzOz6fwq.d.ts +1126 -0
- package/dist/validation.d.ts +1 -0
- package/dist/validation.js +445 -0
- package/dist/validation.js.map +1 -0
- package/package.json +70 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/observability/hook-executor.ts","../../src/runtime/crypto.ts","../../src/observability/trace-context.ts","../../src/observability/otel-attributes.ts","../../src/observability/defaults.ts","../../src/observability/logger.ts","../../src/observability/index.ts"],"sourcesContent":["/**\n * Hook Executor\n *\n * Executes observability hooks with timeout protection and error isolation.\n * Ensures hooks never crash flow execution.\n *\n * @module @doclo/core/observability/hook-executor\n */\n\nimport type { HookError, ObservabilityConfig, HookContext } from './types.js';\n\n/**\n * Default hook timeout in milliseconds\n */\nconst DEFAULT_HOOK_TIMEOUT = 5000;\n\n/**\n * Generic hook function type.\n * The context parameter accepts any hook context type since this executor\n * handles all observability hooks (onFlowStart, onStepEnd, onLog, etc.).\n * Each specific hook in ObservabilityConfig has its own typed signature.\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\ntype GenericHookFunction = (context: any) => void | Promise<void>;\n\n/**\n * Hook execution options\n */\ninterface HookExecutionOptions {\n /** Hook name for error reporting */\n hookName: string;\n /** Observability configuration */\n config: ObservabilityConfig;\n /** Context passed to hook - specific type depends on which hook is being called */\n context: HookContext;\n /** Whether this is a fire-and-forget hook (e.g., onLog) */\n fireAndForget?: boolean;\n}\n\n/**\n * Execute a hook with timeout protection and error isolation\n *\n * @param hook - The hook function to execute\n * @param options - Execution options\n * @returns Promise that resolves when hook completes (or times out)\n */\nexport async function executeHook(\n hook: GenericHookFunction | undefined,\n options: HookExecutionOptions\n): Promise<void> {\n const { hookName, config, context, fireAndForget = false } = options;\n\n // Skip if hook not defined\n if (!hook) {\n return;\n }\n\n // Skip if observability disabled\n if (config.enabled === false) {\n return;\n }\n\n // Fire-and-forget: don't wait for hook\n if (fireAndForget || config.fireAndForget) {\n // Execute hook but don't wait\n executeHookWithErrorHandling(hook, hookName, context, config).catch(() => {\n // Silently ignore errors in fire-and-forget mode\n });\n return;\n }\n\n // Normal execution: wait for hook with timeout\n const timeout = config.hookTimeout ?? DEFAULT_HOOK_TIMEOUT;\n\n try {\n await executeHookWithTimeout(hook, context, timeout, hookName, config);\n } catch (error) {\n // Error already handled in executeHookWithTimeout\n // This catch is just to prevent unhandled rejection\n }\n}\n\n/**\n * Execute hook with error handling\n */\nasync function executeHookWithErrorHandling(\n hook: GenericHookFunction,\n hookName: string,\n context: HookContext,\n config: ObservabilityConfig\n): Promise<void> {\n try {\n const result = hook(context);\n // If hook returns a promise, await it\n if (result && typeof result.then === 'function') {\n await result;\n }\n } catch (error) {\n handleHookError(error as Error, hookName, context, config);\n }\n}\n\n/**\n * Execute hook with timeout protection\n */\nasync function executeHookWithTimeout(\n hook: GenericHookFunction,\n context: HookContext,\n timeoutMs: number,\n hookName: string,\n config: ObservabilityConfig\n): Promise<void> {\n // Create timeout promise\n const timeoutPromise = new Promise<never>((_, reject) => {\n setTimeout(() => {\n reject(new Error(`Hook '${hookName}' exceeded timeout of ${timeoutMs}ms`));\n }, timeoutMs);\n });\n\n // Create hook execution promise\n const hookPromise = executeHookWithErrorHandling(hook, hookName, context, config);\n\n // Race between hook and timeout\n try {\n await Promise.race([hookPromise, timeoutPromise]);\n } catch (error) {\n // Timeout or error occurred\n if (error instanceof Error && error.message.includes('exceeded timeout')) {\n // Timeout error\n handleHookError(error, hookName, context, config);\n }\n // Other errors already handled in executeHookWithErrorHandling\n }\n}\n\n/**\n * Handle hook execution error\n */\nfunction handleHookError(\n error: Error,\n hookName: string,\n context: HookContext,\n config: ObservabilityConfig\n): void {\n const hookError: HookError = {\n hookName,\n error,\n context,\n timestamp: Date.now(),\n };\n\n // Call onHookError if provided\n if (config.onHookError) {\n try {\n config.onHookError(hookError);\n } catch (onHookErrorError) {\n // onHookError itself failed, log to console as fallback\n console.error('[Observability] onHookError handler failed:', onHookErrorError);\n console.error('[Observability] Original hook error:', hookError);\n }\n } else {\n // No onHookError handler, log to console\n console.warn(`[Observability] Hook '${hookName}' failed:`, error);\n }\n\n // If failOnHookError is true, throw the error\n if (config.failOnHookError) {\n throw error;\n }\n\n // Otherwise, silently continue (error isolated)\n}\n\n/**\n * Execute multiple hooks serially with timeout protection\n *\n * This ensures hooks run one at a time in the order they're provided.\n *\n * @param hooks - Array of hook execution configs\n */\nexport async function executeHooksSerial(\n hooks: Array<{\n hook: GenericHookFunction | undefined;\n options: HookExecutionOptions;\n }>\n): Promise<void> {\n for (const { hook, options } of hooks) {\n await executeHook(hook, options);\n }\n}\n\n/**\n * Check if observability is enabled for this execution\n *\n * Takes sampling into account.\n */\nexport function isObservabilityEnabled(config: ObservabilityConfig): boolean {\n // Check if explicitly disabled\n if (config.enabled === false) {\n return false;\n }\n\n // Always enabled if no sampling rate specified\n if (config.samplingRate === undefined || config.samplingRate === 1.0) {\n return true;\n }\n\n // Never sample if rate is 0\n if (config.samplingRate === 0.0) {\n return false;\n }\n\n // Random sampling based on rate\n // Note: This decision is made once per flow execution in flow-builder\n // and stored, so sampling is consistent throughout the execution\n return Math.random() < config.samplingRate;\n}\n","/**\n * Universal Crypto Adapter\n *\n * Provides crypto-secure random byte generation for both Node.js and Edge Runtime.\n * Uses Web Crypto API (available in both environments) for maximum compatibility.\n *\n * @module @doclo/core/runtime/crypto\n */\n\n/**\n * Generate crypto-secure random bytes\n *\n * Uses Web Crypto API which is available in:\n * - Node.js 18+ (globalThis.crypto)\n * - Browsers (window.crypto)\n * - Cloudflare Workers (crypto)\n * - Vercel Edge Functions (crypto)\n *\n * @param length - Number of random bytes to generate\n * @returns Uint8Array containing random bytes\n */\nexport function getRandomBytes(length: number): Uint8Array {\n const bytes = new Uint8Array(length);\n\n // Use Web Crypto API (available in all supported runtimes)\n // Node.js 18+, Edge Runtime, and browsers all support globalThis.crypto\n if (typeof globalThis !== 'undefined' && globalThis.crypto?.getRandomValues) {\n globalThis.crypto.getRandomValues(bytes);\n return bytes;\n }\n\n throw new Error(\n 'Web Crypto API not available. This SDK requires:\\n' +\n '- Node.js 18.0.0 or later (has globalThis.crypto)\\n' +\n '- Edge Runtime (Vercel, Cloudflare)\\n' +\n '- Modern browsers'\n );\n}\n\n/**\n * Convert Uint8Array to lowercase hex string\n *\n * @param bytes - Byte array to convert\n * @returns Hex string representation\n */\nexport function bytesToHex(bytes: Uint8Array): string {\n return Array.from(bytes)\n .map(b => b.toString(16).padStart(2, '0'))\n .join('');\n}\n\n/**\n * Generate random hex string of specified byte length\n *\n * @param byteLength - Number of random bytes (hex string will be 2x this length)\n * @returns Lowercase hex string\n *\n * @example\n * ```typescript\n * const traceId = randomHex(16); // 32 hex characters\n * const spanId = randomHex(8); // 16 hex characters\n * ```\n */\nexport function randomHex(byteLength: number): string {\n const bytes = getRandomBytes(byteLength);\n return bytesToHex(bytes);\n}\n\n/**\n * Generate a UUID v4 string\n *\n * Format: xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx\n *\n * @returns UUID v4 string\n *\n * @example\n * ```typescript\n * const id = randomUUID(); // \"550e8400-e29b-41d4-a716-446655440000\"\n * ```\n */\nexport function randomUUID(): string {\n // Try native crypto.randomUUID() first (Node 19+, all Edge runtimes)\n if (typeof globalThis !== 'undefined' && globalThis.crypto?.randomUUID) {\n return globalThis.crypto.randomUUID();\n }\n\n // Fallback: Manual UUID v4 generation\n const bytes = getRandomBytes(16);\n\n // Set version (4) and variant bits for UUID v4\n bytes[6] = (bytes[6] & 0x0f) | 0x40; // Version 4\n bytes[8] = (bytes[8] & 0x3f) | 0x80; // Variant 10\n\n // Format as UUID string\n const hex = bytesToHex(bytes);\n return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`;\n}\n","/**\n * Trace Context Generator\n *\n * Implements W3C Trace Context standard for distributed tracing.\n * Generates crypto-secure trace IDs, span IDs, and execution IDs.\n *\n * @see https://www.w3.org/TR/trace-context/\n * @module @doclo/core/observability/trace-context\n */\n\nimport { randomHex, randomUUID as cryptoRandomUUID } from '../runtime/crypto.js';\nimport type { TraceContext, TraceContextInput, ObservabilityConfig } from './types.js';\n\n/**\n * W3C Trace Context version (always \"00\")\n */\nconst TRACE_CONTEXT_VERSION = '00';\n\n/**\n * Trace flags for sampled traces\n */\nexport const TRACE_FLAGS_SAMPLED = 0x01;\n\n/**\n * Trace flags for non-sampled traces\n */\nexport const TRACE_FLAGS_NOT_SAMPLED = 0x00;\n\n/**\n * Generate a crypto-secure trace ID (32 lowercase hex characters)\n *\n * Format: 32 hex digits (16 bytes)\n * Example: \"4bf92f3577b34da6a3ce929d0e0e4736\"\n */\nexport function generateTraceId(): string {\n return randomHex(16);\n}\n\n/**\n * Generate a crypto-secure span ID (16 lowercase hex characters)\n *\n * Format: 16 hex digits (8 bytes)\n * Example: \"00f067aa0ba902b7\"\n */\nexport function generateSpanId(): string {\n return randomHex(8);\n}\n\n/**\n * Generate a unique execution ID\n *\n * Uses crypto-secure random bytes for uniqueness.\n * Format: UUID v4 style (xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx)\n */\nexport function generateExecutionId(): string {\n return cryptoRandomUUID();\n}\n\n/**\n * Create a new trace context for flow execution\n *\n * @param config - Observability configuration\n * @param sampled - Whether this execution is sampled\n * @returns Complete trace context\n */\nexport function createTraceContext(\n config: ObservabilityConfig,\n sampled: boolean\n): TraceContext {\n // Use custom generator if provided, otherwise use default\n const traceIdGenerator = config.generateTraceId ?? generateTraceId;\n const spanIdGenerator = config.generateSpanId ?? generateSpanId;\n\n // If trace context propagation is configured, use it\n if (config.traceContext) {\n const input = config.traceContext;\n return {\n traceId: input.traceId,\n spanId: spanIdGenerator(),\n parentSpanId: input.parentSpanId,\n traceFlags: input.traceFlags ?? (sampled ? TRACE_FLAGS_SAMPLED : TRACE_FLAGS_NOT_SAMPLED),\n traceState: input.traceState,\n };\n }\n\n // Create new trace context\n return {\n traceId: traceIdGenerator(),\n spanId: spanIdGenerator(),\n traceFlags: sampled ? TRACE_FLAGS_SAMPLED : TRACE_FLAGS_NOT_SAMPLED,\n };\n}\n\n/**\n * Create a child span context from a parent trace context\n *\n * @param parent - Parent trace context\n * @param config - Observability configuration (for custom span ID generator)\n * @returns New trace context with same traceId but new spanId\n */\nexport function createChildSpanContext(\n parent: TraceContext,\n config?: ObservabilityConfig\n): TraceContext {\n const spanIdGenerator = config?.generateSpanId ?? generateSpanId;\n\n return {\n traceId: parent.traceId,\n spanId: spanIdGenerator(),\n parentSpanId: parent.spanId,\n traceFlags: parent.traceFlags,\n traceState: parent.traceState,\n };\n}\n\n/**\n * Format trace context as W3C traceparent header value\n *\n * Format: \"00-{traceId}-{spanId}-{flags}\"\n * Example: \"00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01\"\n *\n * @see https://www.w3.org/TR/trace-context/#traceparent-header\n */\nexport function formatTraceparent(context: TraceContext): string {\n const flags = context.traceFlags.toString(16).padStart(2, '0');\n return `${TRACE_CONTEXT_VERSION}-${context.traceId}-${context.spanId}-${flags}`;\n}\n\n/**\n * Parse W3C traceparent header value\n *\n * @param traceparent - Traceparent header value\n * @returns Parsed trace context input or null if invalid\n */\nexport function parseTraceparent(traceparent: string): TraceContextInput | null {\n // Format: \"00-{traceId}-{spanId}-{flags}\"\n const parts = traceparent.split('-');\n\n if (parts.length !== 4) {\n return null;\n }\n\n const [version, traceId, spanId, flags] = parts;\n\n // Validate version\n if (version !== TRACE_CONTEXT_VERSION) {\n return null;\n }\n\n // Validate traceId (32 hex chars)\n if (!/^[0-9a-f]{32}$/.test(traceId)) {\n return null;\n }\n\n // Validate spanId (16 hex chars)\n if (!/^[0-9a-f]{16}$/.test(spanId)) {\n return null;\n }\n\n // Validate flags (2 hex chars)\n if (!/^[0-9a-f]{2}$/.test(flags)) {\n return null;\n }\n\n return {\n traceId,\n parentSpanId: spanId,\n traceFlags: parseInt(flags, 16),\n };\n}\n\n/**\n * Format trace context as W3C tracestate header value\n *\n * @see https://www.w3.org/TR/trace-context/#tracestate-header\n */\nexport function formatTracestate(context: TraceContext): string | undefined {\n return context.traceState;\n}\n\n/**\n * Check if trace context is sampled\n */\nexport function isTraceSampled(context: TraceContext): boolean {\n return (context.traceFlags & TRACE_FLAGS_SAMPLED) === TRACE_FLAGS_SAMPLED;\n}\n\n/**\n * Validate trace ID format\n *\n * Must be 32 lowercase hex characters, not all zeros\n */\nexport function isValidTraceId(traceId: string): boolean {\n // Must be 32 hex characters\n if (!/^[0-9a-f]{32}$/.test(traceId)) {\n return false;\n }\n\n // Must not be all zeros\n if (traceId === '00000000000000000000000000000000') {\n return false;\n }\n\n return true;\n}\n\n/**\n * Validate span ID format\n *\n * Must be 16 lowercase hex characters, not all zeros\n */\nexport function isValidSpanId(spanId: string): boolean {\n // Must be 16 hex characters\n if (!/^[0-9a-f]{16}$/.test(spanId)) {\n return false;\n }\n\n // Must not be all zeros\n if (spanId === '0000000000000000') {\n return false;\n }\n\n return true;\n}\n\n/**\n * Trace Context Manager\n *\n * Manages trace context for the current execution.\n */\nexport class TraceContextManager {\n private traceContext: TraceContext | null = null;\n private config: ObservabilityConfig;\n\n constructor(config: ObservabilityConfig) {\n this.config = config;\n }\n\n /**\n * Initialize trace context for new execution\n */\n initialize(sampled: boolean): TraceContext {\n this.traceContext = createTraceContext(this.config, sampled);\n return this.traceContext;\n }\n\n /**\n * Get current trace context\n */\n getTraceContext(): TraceContext | null {\n return this.traceContext;\n }\n\n /**\n * Create child span context\n */\n createChildSpan(): TraceContext {\n if (!this.traceContext) {\n throw new Error('Trace context not initialized');\n }\n return createChildSpanContext(this.traceContext, this.config);\n }\n\n /**\n * Get traceparent header value\n */\n getTraceparent(): string | null {\n if (!this.traceContext) {\n return null;\n }\n return formatTraceparent(this.traceContext);\n }\n\n /**\n * Reset trace context (for testing)\n */\n reset(): void {\n this.traceContext = null;\n }\n}\n","/**\n * OpenTelemetry Semantic Conventions for Gen AI\n *\n * Implements Gen AI semantic conventions (v1.29.0) for observability.\n * Maps SDK data to standard OpenTelemetry attributes.\n *\n * @see https://opentelemetry.io/docs/specs/semconv/gen-ai/\n * @module @doclo/core/observability/otel-attributes\n */\n\n/**\n * Gen AI system names (standardized)\n */\nconst GEN_AI_SYSTEMS: Record<string, string> = {\n openai: 'openai',\n 'openai-compatible': 'openai',\n anthropic: 'anthropic',\n google: 'vertex_ai', // Google uses Vertex AI\n 'google-ai': 'vertex_ai',\n cohere: 'cohere',\n huggingface: 'huggingface',\n openrouter: 'openrouter', // Custom system\n ollama: 'ollama', // Custom system\n};\n\n/**\n * Map provider name to Gen AI system identifier\n */\nfunction mapProviderToSystem(provider: string): string {\n const normalized = provider.toLowerCase();\n return GEN_AI_SYSTEMS[normalized] ?? normalized;\n}\n\n/**\n * Build OpenTelemetry attributes for LLM operations\n *\n * @param data - Step execution data\n * @returns OpenTelemetry attributes object\n */\nexport function buildOtelAttributes(data: {\n provider?: string;\n model?: string;\n modelUsed?: string;\n stepType?: string;\n inputTokens?: number;\n outputTokens?: number;\n finishReason?: string;\n temperature?: number;\n maxTokens?: number;\n topP?: number;\n topK?: number;\n}): Record<string, string | number | boolean> {\n const attributes: Record<string, string | number | boolean> = {};\n\n // Gen AI system (required)\n if (data.provider) {\n attributes['gen_ai.system'] = mapProviderToSystem(data.provider);\n }\n\n // Operation name (what type of operation)\n if (data.stepType) {\n attributes['gen_ai.operation.name'] = data.stepType;\n }\n\n // Request model (requested model name)\n if (data.model) {\n attributes['gen_ai.request.model'] = data.model;\n }\n\n // Response model (actual model used, may differ from requested)\n if (data.modelUsed) {\n attributes['gen_ai.response.model'] = data.modelUsed;\n }\n\n // Token usage\n if (data.inputTokens !== undefined) {\n attributes['gen_ai.usage.input_tokens'] = data.inputTokens;\n }\n\n if (data.outputTokens !== undefined) {\n attributes['gen_ai.usage.output_tokens'] = data.outputTokens;\n }\n\n // Finish reason (simplified as single string, not array)\n if (data.finishReason) {\n attributes['gen_ai.response.finish_reason'] = data.finishReason;\n }\n\n // Request parameters\n if (data.temperature !== undefined) {\n attributes['gen_ai.request.temperature'] = data.temperature;\n }\n\n if (data.maxTokens !== undefined) {\n attributes['gen_ai.request.max_tokens'] = data.maxTokens;\n }\n\n if (data.topP !== undefined) {\n attributes['gen_ai.request.top_p'] = data.topP;\n }\n\n if (data.topK !== undefined) {\n attributes['gen_ai.request.top_k'] = data.topK;\n }\n\n return attributes;\n}\n\n/**\n * Build OpenTelemetry attributes for provider requests\n *\n * Used for onProviderRequest hooks\n */\nexport function buildProviderRequestAttributes(data: {\n provider: string;\n model: string;\n config?: {\n temperature?: number;\n maxTokens?: number;\n topP?: number;\n topK?: number;\n };\n}): Record<string, string | number | boolean> {\n return buildOtelAttributes({\n provider: data.provider,\n model: data.model,\n temperature: data.config?.temperature,\n maxTokens: data.config?.maxTokens,\n topP: data.config?.topP,\n topK: data.config?.topK,\n });\n}\n\n/**\n * Build OpenTelemetry attributes for provider responses\n *\n * Used for onProviderResponse hooks\n */\nexport function buildProviderResponseAttributes(data: {\n provider: string;\n model: string;\n modelUsed?: string;\n inputTokens: number;\n outputTokens: number;\n finishReason?: string;\n}): Record<string, string | number | boolean> {\n return buildOtelAttributes({\n provider: data.provider,\n model: data.model,\n modelUsed: data.modelUsed,\n inputTokens: data.inputTokens,\n outputTokens: data.outputTokens,\n finishReason: data.finishReason,\n });\n}\n\n/**\n * Build OpenTelemetry attributes for step execution\n *\n * Used for onStepEnd hooks\n */\nexport function buildStepAttributes(data: {\n stepType: string;\n provider?: string;\n model?: string;\n modelUsed?: string;\n inputTokens?: number;\n outputTokens?: number;\n finishReason?: string;\n config?: {\n temperature?: number;\n maxTokens?: number;\n topP?: number;\n topK?: number;\n };\n}): Record<string, string | number | boolean> {\n return buildOtelAttributes({\n stepType: data.stepType,\n provider: data.provider,\n model: data.model,\n modelUsed: data.modelUsed,\n inputTokens: data.inputTokens,\n outputTokens: data.outputTokens,\n finishReason: data.finishReason,\n temperature: data.config?.temperature,\n maxTokens: data.config?.maxTokens,\n topP: data.config?.topP,\n topK: data.config?.topK,\n });\n}\n\n/**\n * Standard finish reason mappings\n *\n * Maps provider-specific finish reasons to standard values\n */\nexport const FINISH_REASON_MAPPING: Record<string, string> = {\n // OpenAI\n stop: 'stop',\n length: 'length',\n content_filter: 'content_filter',\n tool_calls: 'tool_calls',\n function_call: 'function_call',\n\n // Anthropic\n end_turn: 'stop',\n max_tokens: 'length',\n stop_sequence: 'stop',\n\n // Google\n STOP: 'stop',\n MAX_TOKENS: 'length',\n SAFETY: 'content_filter',\n RECITATION: 'content_filter',\n\n // Generic\n complete: 'stop',\n truncated: 'length',\n filtered: 'content_filter',\n};\n\n/**\n * Normalize finish reason to standard value\n */\nexport function normalizeFinishReason(reason: string | undefined): string | undefined {\n if (!reason) {\n return undefined;\n }\n\n return FINISH_REASON_MAPPING[reason] ?? reason;\n}\n\n/**\n * Add standard OpenTelemetry span attributes\n *\n * These are generic attributes that apply to all spans\n */\nexport function addStandardSpanAttributes(attributes: Record<string, string | number | boolean>, data: {\n spanKind?: 'client' | 'server' | 'internal';\n serviceName?: string;\n serviceVersion?: string;\n}): void {\n if (data.spanKind) {\n attributes['span.kind'] = data.spanKind;\n }\n\n if (data.serviceName) {\n attributes['service.name'] = data.serviceName;\n }\n\n if (data.serviceVersion) {\n attributes['service.version'] = data.serviceVersion;\n }\n}\n\n/**\n * Build full OpenTelemetry context for export\n *\n * Combines Gen AI attributes with standard span attributes\n */\nexport function buildFullOtelContext(data: {\n // Gen AI attributes\n stepType?: string;\n provider?: string;\n model?: string;\n modelUsed?: string;\n inputTokens?: number;\n outputTokens?: number;\n finishReason?: string;\n config?: {\n temperature?: number;\n maxTokens?: number;\n topP?: number;\n topK?: number;\n };\n\n // Standard span attributes\n spanKind?: 'client' | 'server' | 'internal';\n serviceName?: string;\n serviceVersion?: string;\n}): Record<string, string | number | boolean> {\n const attributes = buildOtelAttributes({\n stepType: data.stepType,\n provider: data.provider,\n model: data.model,\n modelUsed: data.modelUsed,\n inputTokens: data.inputTokens,\n outputTokens: data.outputTokens,\n finishReason: data.finishReason,\n temperature: data.config?.temperature,\n maxTokens: data.config?.maxTokens,\n topP: data.config?.topP,\n topK: data.config?.topK,\n });\n\n addStandardSpanAttributes(attributes, {\n spanKind: data.spanKind,\n serviceName: data.serviceName,\n serviceVersion: data.serviceVersion,\n });\n\n return attributes;\n}\n","/**\n * Observability Configuration Defaults\n *\n * Provides default configuration values and config merging utilities.\n *\n * @module @doclo/core/observability/defaults\n */\n\nimport type { ObservabilityConfig } from './types.js';\n\n/**\n * Default observability configuration\n */\nexport const DEFAULT_OBSERVABILITY_CONFIG: Required<Omit<ObservabilityConfig,\n | 'onFlowStart' | 'onFlowEnd' | 'onFlowError'\n | 'onStepStart' | 'onStepEnd' | 'onStepError'\n | 'onConsensusStart' | 'onConsensusRunRetry' | 'onConsensusRunComplete' | 'onConsensusComplete'\n | 'onBatchStart' | 'onBatchItemStart' | 'onBatchItemEnd' | 'onBatchEnd'\n | 'onProviderRequest' | 'onProviderResponse' | 'onProviderRetry' | 'onCircuitBreakerTriggered'\n | 'onLog' | 'onHookError'\n | 'customSampler' | 'generateTraceId' | 'generateSpanId' | 'generateExecutionId'\n | 'traceContext' | 'observabilityVersion'\n>> = {\n enabled: true,\n samplingRate: 1.0,\n samplingStrategy: 'random',\n asyncHooks: true,\n fireAndForget: false,\n hookTimeout: 5000,\n failOnHookError: false,\n};\n\n/**\n * Merge user config with defaults\n *\n * User config takes precedence over defaults.\n */\nexport function mergeConfig(userConfig?: ObservabilityConfig): ObservabilityConfig {\n if (!userConfig) {\n return { ...DEFAULT_OBSERVABILITY_CONFIG };\n }\n\n return {\n ...DEFAULT_OBSERVABILITY_CONFIG,\n ...userConfig,\n };\n}\n\n/**\n * Determine if execution should be sampled\n *\n * Uses sampling strategy from config.\n */\nexport function shouldSample(config: ObservabilityConfig): boolean {\n // Always sample if no rate specified\n if (config.samplingRate === undefined || config.samplingRate === 1.0) {\n return true;\n }\n\n // Never sample if rate is 0\n if (config.samplingRate === 0.0) {\n return false;\n }\n\n // Custom sampler (will be called later with flow context)\n if (config.samplingStrategy === 'custom' && config.customSampler) {\n // Custom sampler will be called in flow execution\n // For now, assume sampled (decision made later)\n return true;\n }\n\n // Random sampling (default)\n return Math.random() < config.samplingRate;\n}\n\n/**\n * Validate observability configuration\n *\n * Checks for invalid values and warns about issues.\n */\nexport function validateConfig(config: ObservabilityConfig): {\n valid: boolean;\n warnings: string[];\n errors: string[];\n} {\n const warnings: string[] = [];\n const errors: string[] = [];\n\n // Validate sampling rate\n if (config.samplingRate !== undefined) {\n if (config.samplingRate < 0 || config.samplingRate > 1) {\n errors.push('samplingRate must be between 0.0 and 1.0');\n }\n }\n\n // Validate hook timeout\n if (config.hookTimeout !== undefined) {\n if (config.hookTimeout <= 0) {\n errors.push('hookTimeout must be positive');\n }\n if (config.hookTimeout > 60000) {\n warnings.push('hookTimeout > 60s may cause long execution delays');\n }\n }\n\n // Validate sampling strategy\n if (config.samplingStrategy === 'custom' && !config.customSampler) {\n errors.push('customSampler required when samplingStrategy is \"custom\"');\n }\n\n // Validate trace context propagation\n if (config.traceContext) {\n const { traceId, parentSpanId } = config.traceContext;\n\n // Validate trace ID format\n if (!/^[0-9a-f]{32}$/.test(traceId)) {\n errors.push('traceContext.traceId must be 32 lowercase hex characters');\n }\n\n // Validate parent span ID format\n if (!/^[0-9a-f]{16}$/.test(parentSpanId)) {\n errors.push('traceContext.parentSpanId must be 16 lowercase hex characters');\n }\n\n // Validate trace ID is not all zeros\n if (traceId === '00000000000000000000000000000000') {\n errors.push('traceContext.traceId cannot be all zeros');\n }\n\n // Validate parent span ID is not all zeros\n if (parentSpanId === '0000000000000000') {\n errors.push('traceContext.parentSpanId cannot be all zeros');\n }\n }\n\n // Warn about conflicting options\n if (config.asyncHooks === false && config.hookTimeout !== undefined) {\n warnings.push('hookTimeout has no effect when asyncHooks is false');\n }\n\n if (config.fireAndForget === true && config.hookTimeout !== undefined) {\n warnings.push('hookTimeout has no effect when fireAndForget is true');\n }\n\n if (config.enabled === false && Object.keys(config).length > 1) {\n warnings.push('Observability is disabled, other config options have no effect');\n }\n\n return {\n valid: errors.length === 0,\n warnings,\n errors,\n };\n}\n\n/**\n * Get observability version\n *\n * Returns the version of the observability contract.\n */\nexport function getObservabilityVersion(config?: ObservabilityConfig): string {\n return config?.observabilityVersion ?? '1.0.0';\n}\n\n/**\n * Check if observability is effectively disabled\n *\n * Returns true if observability is explicitly disabled or sampling rate is 0.\n */\nexport function isObservabilityDisabled(config: ObservabilityConfig): boolean {\n return config.enabled === false || config.samplingRate === 0.0;\n}\n\n/**\n * Check if any hooks are configured\n *\n * Returns true if at least one hook is defined.\n */\nexport function hasAnyHooks(config: ObservabilityConfig): boolean {\n return !!(\n config.onFlowStart ||\n config.onFlowEnd ||\n config.onFlowError ||\n config.onStepStart ||\n config.onStepEnd ||\n config.onStepError ||\n config.onConsensusStart ||\n config.onConsensusRunRetry ||\n config.onConsensusRunComplete ||\n config.onConsensusComplete ||\n config.onBatchStart ||\n config.onBatchItemStart ||\n config.onBatchItemEnd ||\n config.onBatchEnd ||\n config.onProviderRequest ||\n config.onProviderResponse ||\n config.onProviderRetry ||\n config.onCircuitBreakerTriggered ||\n config.onLog\n );\n}\n\n/**\n * Create minimal config for testing\n *\n * Useful for unit tests that don't need full observability.\n */\nexport function createMinimalConfig(overrides?: Partial<ObservabilityConfig>): ObservabilityConfig {\n return {\n enabled: true,\n samplingRate: 1.0,\n asyncHooks: false, // Faster for tests\n fireAndForget: false,\n hookTimeout: 1000, // Shorter for tests\n failOnHookError: false,\n ...overrides,\n };\n}\n","/**\n * Logging Utility for Observability\n *\n * Provides a logger that integrates with the observability system.\n * Logs are sent to the onLog hook with fire-and-forget execution.\n *\n * @module @doclo/core/observability/logger\n */\n\nimport type { ObservabilityConfig, LogContext, TraceContext } from './types.js';\nimport { executeHook } from './hook-executor.js';\n\nexport type LogLevel = 'debug' | 'info' | 'warn' | 'error';\n\nexport interface LoggerOptions {\n observability?: ObservabilityConfig;\n flowId?: string;\n executionId?: string;\n stepId?: string;\n traceContext?: TraceContext;\n metadata?: Record<string, unknown>;\n}\n\n/**\n * Logger class that integrates with observability hooks\n */\nexport class Logger {\n private options: LoggerOptions;\n\n constructor(options: LoggerOptions = {}) {\n this.options = options;\n }\n\n /**\n * Log a debug message\n */\n debug(message: string, data?: unknown): void {\n this.log('debug', message, data);\n }\n\n /**\n * Log an info message\n */\n info(message: string, data?: unknown): void {\n this.log('info', message, data);\n }\n\n /**\n * Log a warning message\n */\n warn(message: string, data?: unknown): void {\n this.log('warn', message, data);\n }\n\n /**\n * Log an error message\n */\n error(message: string, error?: Error | unknown, data?: unknown): void {\n this.log('error', message, data, error instanceof Error ? error : undefined);\n }\n\n /**\n * Internal log method that sends to onLog hook\n */\n private log(level: LogLevel, message: string, data?: unknown, error?: Error): void {\n const timestamp = Date.now();\n\n // Always output to console as fallback\n const consoleMethod = level === 'error' ? console.error : level === 'warn' ? console.warn : console.log;\n if (error) {\n consoleMethod(`[${level.toUpperCase()}] ${message}`, data, error);\n } else if (data !== undefined) {\n consoleMethod(`[${level.toUpperCase()}] ${message}`, data);\n } else {\n consoleMethod(`[${level.toUpperCase()}] ${message}`);\n }\n\n // Send to onLog hook (fire-and-forget)\n if (this.options.observability?.onLog && this.options.traceContext) {\n // Combine data and error into metadata\n const combinedMetadata: Record<string, unknown> = {\n ...(this.options.metadata || {}),\n };\n if (data !== undefined) {\n combinedMetadata.data = data;\n }\n if (error) {\n combinedMetadata.error = {\n name: error.name,\n message: error.message,\n stack: error.stack,\n };\n }\n\n const logContext: LogContext = {\n flowId: this.options.flowId ?? 'unknown',\n executionId: this.options.executionId ?? 'unknown',\n stepId: this.options.stepId,\n timestamp,\n level,\n message,\n metadata: combinedMetadata,\n traceContext: this.options.traceContext,\n };\n\n // Fire-and-forget execution (don't await)\n executeHook(this.options.observability.onLog, {\n hookName: 'onLog',\n config: this.options.observability,\n context: logContext,\n fireAndForget: true, // Special handling for onLog\n }).catch(() => {\n // Silently ignore onLog errors\n });\n }\n }\n}\n\n/**\n * Create a logger instance with observability integration\n */\nexport function createLogger(options: LoggerOptions = {}): Logger {\n return new Logger(options);\n}\n","/**\n * Observability Module\n *\n * Comprehensive observability system for the doclo-sdk SDK.\n * Provides hooks, tracing, and monitoring capabilities.\n *\n * @module @doclo/core/observability\n * @example\n * ```typescript\n * import { ObservabilityConfig } from '@doclo/core/observability';\n *\n * const config: ObservabilityConfig = {\n * onFlowStart: (ctx) => console.log('Flow started:', ctx.flowId),\n * onStepEnd: (ctx) => console.log('Step completed:', ctx.stepId, ctx.duration),\n * };\n * ```\n */\n\n// ============================================================================\n// Type Exports\n// ============================================================================\n\nexport type {\n // Main configuration\n ObservabilityConfig,\n\n // Flow-level contexts\n FlowStartContext,\n FlowEndContext,\n FlowErrorContext,\n FlowStats,\n\n // Step-level contexts\n StepStartContext,\n StepEndContext,\n StepErrorContext,\n\n // Consensus contexts\n ConsensusStartContext,\n ConsensusRunRetryContext,\n ConsensusRunContext,\n ConsensusCompleteContext,\n\n // Batch/forEach contexts\n BatchStartContext,\n BatchItemContext,\n BatchItemEndContext,\n BatchEndContext,\n\n // Provider-level contexts\n ProviderRequestContext,\n ProviderResponseContext,\n ProviderRetryContext,\n CircuitBreakerContext,\n\n // Logging context\n LogContext,\n\n // Error handling\n HookError,\n\n // Trace context\n TraceContext,\n TraceContextInput,\n\n // Execution context\n ExecutionContext,\n CustomMetric,\n} from './types.js';\n\n// ============================================================================\n// Hook Executor\n// ============================================================================\n\n// Note: Hook executor is for SDK internal use (not for end users)\n// It's used by the SDK to execute hooks with timeout/error protection\n\nexport {\n executeHook,\n executeHooksSerial,\n isObservabilityEnabled,\n} from './hook-executor.js';\n\n// ============================================================================\n// Trace Context Utilities\n// ============================================================================\n\nexport {\n generateTraceId,\n generateSpanId,\n generateExecutionId,\n createTraceContext,\n createChildSpanContext,\n formatTraceparent,\n parseTraceparent,\n formatTracestate,\n isTraceSampled,\n isValidTraceId,\n isValidSpanId,\n TraceContextManager,\n TRACE_FLAGS_SAMPLED,\n TRACE_FLAGS_NOT_SAMPLED,\n} from './trace-context.js';\n\n// ============================================================================\n// OpenTelemetry Attributes\n// ============================================================================\n\nexport {\n buildOtelAttributes,\n buildProviderRequestAttributes,\n buildProviderResponseAttributes,\n buildStepAttributes,\n buildFullOtelContext,\n normalizeFinishReason,\n addStandardSpanAttributes,\n FINISH_REASON_MAPPING,\n} from './otel-attributes.js';\n\n// ============================================================================\n// Configuration Defaults\n// ============================================================================\n\nexport {\n DEFAULT_OBSERVABILITY_CONFIG,\n mergeConfig,\n shouldSample,\n validateConfig,\n getObservabilityVersion,\n isObservabilityDisabled,\n hasAnyHooks,\n createMinimalConfig,\n} from './defaults.js';\n\n// ============================================================================\n// Logger\n// ============================================================================\n\nexport {\n Logger,\n createLogger,\n type LogLevel,\n type LoggerOptions,\n} from './logger.js';\n\n// ============================================================================\n// Version\n// ============================================================================\n\n/**\n * Observability module version\n */\nexport const OBSERVABILITY_VERSION = '1.0.0';\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { D as DocumentIR } from './validation-CzOz6fwq.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* PDF Utilities
|
|
5
|
+
*
|
|
6
|
+
* Edge Runtime compatible PDF manipulation utilities using pdf-lib.
|
|
7
|
+
* These functions work in Node.js, Vercel Edge Functions, Cloudflare Workers, and browsers.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Get the total number of pages in a PDF document
|
|
12
|
+
*
|
|
13
|
+
* @param dataUrl - PDF data URI in format: data:application/pdf;base64,{base64data}
|
|
14
|
+
* @returns Total page count
|
|
15
|
+
* @throws {Error} If the input is not a valid PDF data URL
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```typescript
|
|
19
|
+
* const pageCount = await getPDFPageCount('data:application/pdf;base64,JVBERi0...');
|
|
20
|
+
* console.log(`PDF has ${pageCount} pages`);
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
declare function getPDFPageCount(dataUrl: string): Promise<number>;
|
|
24
|
+
/**
|
|
25
|
+
* Split a PDF into multiple smaller PDFs based on page ranges
|
|
26
|
+
*
|
|
27
|
+
* @param dataUrl - PDF data URI in format: data:application/pdf;base64,{base64data}
|
|
28
|
+
* @param pageRanges - Array of [startPage, endPage] tuples (1-indexed, inclusive)
|
|
29
|
+
* @returns Array of PDF data URLs, one for each page range
|
|
30
|
+
* @throws {Error} If the input is not a valid PDF data URL or page ranges are invalid
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* ```typescript
|
|
34
|
+
* // Split a 10-page PDF into three chunks
|
|
35
|
+
* const chunks = await splitPDFIntoChunks(pdfDataUrl, [
|
|
36
|
+
* [1, 3], // Pages 1-3
|
|
37
|
+
* [4, 7], // Pages 4-7
|
|
38
|
+
* [8, 10] // Pages 8-10
|
|
39
|
+
* ]);
|
|
40
|
+
* console.log(`Created ${chunks.length} PDF chunks`);
|
|
41
|
+
* ```
|
|
42
|
+
*/
|
|
43
|
+
declare function splitPDFIntoChunks(dataUrl: string, pageRanges: Array<[number, number]>): Promise<string[]>;
|
|
44
|
+
/**
|
|
45
|
+
* Get the page count from a DocumentIR, with fallback logic
|
|
46
|
+
*
|
|
47
|
+
* This helper function checks multiple sources for page count:
|
|
48
|
+
* 1. `extras.pageCount` (explicit page count from provider or PDF analysis)
|
|
49
|
+
* 2. `pages.length` (fallback - number of pages in the IR)
|
|
50
|
+
*
|
|
51
|
+
* Note: For Unsiloed provider, `pages.length` represents semantic chunks,
|
|
52
|
+
* not traditional pages. Use `extras.totalSemanticChunks` to distinguish.
|
|
53
|
+
*
|
|
54
|
+
* @param ir - DocumentIR to get page count from
|
|
55
|
+
* @returns Page count (or chunk count for Unsiloed)
|
|
56
|
+
*
|
|
57
|
+
* @example
|
|
58
|
+
* ```typescript
|
|
59
|
+
* const ir = await parseNode.run(pdfUrl, { provider: ocrProvider });
|
|
60
|
+
* const pageCount = getDocumentPageCount(ir);
|
|
61
|
+
* console.log(`Document has ${pageCount} pages`);
|
|
62
|
+
* ```
|
|
63
|
+
*/
|
|
64
|
+
declare function getDocumentPageCount(ir: DocumentIR): number;
|
|
65
|
+
/**
|
|
66
|
+
* Get total page count across multiple DocumentIR objects (chunked results)
|
|
67
|
+
*
|
|
68
|
+
* For chunked parsing results, this sums up the page counts across all chunks.
|
|
69
|
+
* It respects `extras.pageCount` if available, otherwise uses `pages.length`.
|
|
70
|
+
*
|
|
71
|
+
* @param irArray - Array of DocumentIR objects from chunked parsing
|
|
72
|
+
* @returns Total page count across all chunks
|
|
73
|
+
*
|
|
74
|
+
* @example
|
|
75
|
+
* ```typescript
|
|
76
|
+
* const chunks = await parseNode.run(largePdfUrl, {
|
|
77
|
+
* provider: ocrProvider,
|
|
78
|
+
* chunked: { maxPagesPerChunk: 10 }
|
|
79
|
+
* });
|
|
80
|
+
* const totalPages = getTotalPageCount(chunks);
|
|
81
|
+
* console.log(`Total pages across ${chunks.length} chunks: ${totalPages}`);
|
|
82
|
+
* ```
|
|
83
|
+
*/
|
|
84
|
+
declare function getTotalPageCount(irArray: DocumentIR[]): number;
|
|
85
|
+
/**
|
|
86
|
+
* Get comprehensive page-related metadata from a DocumentIR
|
|
87
|
+
*
|
|
88
|
+
* Returns detailed information about page counts, chunk information,
|
|
89
|
+
* and whether the result is chunked or a complete document.
|
|
90
|
+
*
|
|
91
|
+
* @param ir - DocumentIR to analyze
|
|
92
|
+
* @returns Metadata object with page count details
|
|
93
|
+
*
|
|
94
|
+
* @example
|
|
95
|
+
* ```typescript
|
|
96
|
+
* const metadata = getPageCountMetadata(ir);
|
|
97
|
+
* console.log(`Document has ${metadata.pageCount} pages`);
|
|
98
|
+
* if (metadata.isChunked) {
|
|
99
|
+
* console.log(`This is chunk ${metadata.chunkIndex + 1} of ${metadata.totalChunks}`);
|
|
100
|
+
* console.log(`Contains pages ${metadata.pageRange[0]} to ${metadata.pageRange[1]}`);
|
|
101
|
+
* }
|
|
102
|
+
* ```
|
|
103
|
+
*/
|
|
104
|
+
declare function getPageCountMetadata(ir: DocumentIR): {
|
|
105
|
+
/** Total page count (or chunk count for Unsiloed) */
|
|
106
|
+
pageCount: number;
|
|
107
|
+
/** Number of pages in the IR (may differ from pageCount for chunked docs) */
|
|
108
|
+
pagesInIR: number;
|
|
109
|
+
/** Whether this is a chunked result */
|
|
110
|
+
isChunked: boolean;
|
|
111
|
+
/** For chunked results: which chunk this is (0-indexed) */
|
|
112
|
+
chunkIndex?: number;
|
|
113
|
+
/** For chunked results: total number of chunks */
|
|
114
|
+
totalChunks?: number;
|
|
115
|
+
/** For chunked results: page range [start, end] (1-indexed, inclusive) */
|
|
116
|
+
pageRange?: [number, number];
|
|
117
|
+
/** For Unsiloed: total semantic chunks */
|
|
118
|
+
totalSemanticChunks?: number;
|
|
119
|
+
/** Whether this is from Unsiloed (semantic chunking, not traditional pages) */
|
|
120
|
+
isSemanticChunking: boolean;
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
export { getDocumentPageCount, getPDFPageCount, getPageCountMetadata, getTotalPageCount, splitPDFIntoChunks };
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// src/pdf-utils.ts
|
|
2
|
+
import { PDFDocument } from "pdf-lib";
|
|
3
|
+
|
|
4
|
+
// src/runtime/base64.ts
|
|
5
|
+
function arrayBufferToBase64(buffer) {
|
|
6
|
+
if (typeof Buffer !== "undefined") {
|
|
7
|
+
return Buffer.from(buffer).toString("base64");
|
|
8
|
+
}
|
|
9
|
+
const bytes = new Uint8Array(buffer);
|
|
10
|
+
let binary = "";
|
|
11
|
+
for (let i = 0; i < bytes.byteLength; i++) {
|
|
12
|
+
binary += String.fromCharCode(bytes[i]);
|
|
13
|
+
}
|
|
14
|
+
return btoa(binary);
|
|
15
|
+
}
|
|
16
|
+
function base64ToArrayBuffer(base64) {
|
|
17
|
+
const cleanBase64 = base64.replace(/^data:[^;]+;base64,/, "");
|
|
18
|
+
if (typeof Buffer !== "undefined") {
|
|
19
|
+
const buffer = Buffer.from(cleanBase64, "base64");
|
|
20
|
+
return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
|
|
21
|
+
}
|
|
22
|
+
const binaryString = atob(cleanBase64);
|
|
23
|
+
const bytes = new Uint8Array(binaryString.length);
|
|
24
|
+
for (let i = 0; i < binaryString.length; i++) {
|
|
25
|
+
bytes[i] = binaryString.charCodeAt(i);
|
|
26
|
+
}
|
|
27
|
+
return bytes.buffer;
|
|
28
|
+
}
|
|
29
|
+
function uint8ArrayToBase64(bytes) {
|
|
30
|
+
return arrayBufferToBase64(bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// src/pdf-utils.ts
|
|
34
|
+
async function getPDFPageCount(dataUrl) {
|
|
35
|
+
const base64Match = dataUrl.match(/^data:application\/pdf;base64,(.+)$/);
|
|
36
|
+
if (!base64Match) {
|
|
37
|
+
throw new Error("Invalid PDF data URL format. Expected: data:application/pdf;base64,{base64data}");
|
|
38
|
+
}
|
|
39
|
+
const base64Data = base64Match[1];
|
|
40
|
+
const pdfBytes = base64ToArrayBuffer(base64Data);
|
|
41
|
+
const pdfDoc = await PDFDocument.load(pdfBytes);
|
|
42
|
+
return pdfDoc.getPageCount();
|
|
43
|
+
}
|
|
44
|
+
async function splitPDFIntoChunks(dataUrl, pageRanges) {
|
|
45
|
+
const base64Match = dataUrl.match(/^data:application\/pdf;base64,(.+)$/);
|
|
46
|
+
if (!base64Match) {
|
|
47
|
+
throw new Error("Invalid PDF data URL format. Expected: data:application/pdf;base64,{base64data}");
|
|
48
|
+
}
|
|
49
|
+
const base64Data = base64Match[1];
|
|
50
|
+
const pdfBytes = base64ToArrayBuffer(base64Data);
|
|
51
|
+
const pdfDoc = await PDFDocument.load(pdfBytes);
|
|
52
|
+
const totalPages = pdfDoc.getPageCount();
|
|
53
|
+
const chunks = [];
|
|
54
|
+
for (const [startPage, endPage] of pageRanges) {
|
|
55
|
+
if (startPage < 1 || endPage > totalPages || startPage > endPage) {
|
|
56
|
+
throw new Error(
|
|
57
|
+
`Invalid page range [${startPage}, ${endPage}] for PDF with ${totalPages} pages. Page numbers must be 1-indexed and within bounds.`
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
const chunkDoc = await PDFDocument.create();
|
|
61
|
+
const pagesToCopy = Array.from(
|
|
62
|
+
{ length: endPage - startPage + 1 },
|
|
63
|
+
(_, i) => startPage - 1 + i
|
|
64
|
+
// Convert to 0-indexed
|
|
65
|
+
);
|
|
66
|
+
const copiedPages = await chunkDoc.copyPages(pdfDoc, pagesToCopy);
|
|
67
|
+
copiedPages.forEach((page) => chunkDoc.addPage(page));
|
|
68
|
+
const chunkBytes = await chunkDoc.save();
|
|
69
|
+
const chunkBase64 = uint8ArrayToBase64(chunkBytes);
|
|
70
|
+
chunks.push(`data:application/pdf;base64,${chunkBase64}`);
|
|
71
|
+
}
|
|
72
|
+
return chunks;
|
|
73
|
+
}
|
|
74
|
+
function getDocumentPageCount(ir) {
|
|
75
|
+
if (ir.extras?.pageCount !== void 0) {
|
|
76
|
+
return ir.extras.pageCount;
|
|
77
|
+
}
|
|
78
|
+
return ir.pages.length;
|
|
79
|
+
}
|
|
80
|
+
function getTotalPageCount(irArray) {
|
|
81
|
+
return irArray.reduce((sum, ir) => sum + getDocumentPageCount(ir), 0);
|
|
82
|
+
}
|
|
83
|
+
function getPageCountMetadata(ir) {
|
|
84
|
+
const pagesInIR = ir.pages.length;
|
|
85
|
+
const pageCount = ir.extras?.pageCount ?? pagesInIR;
|
|
86
|
+
const isSemanticChunking = ir.extras?.totalSemanticChunks !== void 0;
|
|
87
|
+
const isChunked = ir.extras?.chunkIndex !== void 0 && ir.extras?.totalChunks !== void 0;
|
|
88
|
+
return {
|
|
89
|
+
pageCount,
|
|
90
|
+
pagesInIR,
|
|
91
|
+
isChunked,
|
|
92
|
+
chunkIndex: ir.extras?.chunkIndex,
|
|
93
|
+
totalChunks: ir.extras?.totalChunks,
|
|
94
|
+
pageRange: ir.extras?.pageRange,
|
|
95
|
+
totalSemanticChunks: ir.extras?.totalSemanticChunks,
|
|
96
|
+
isSemanticChunking
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
export {
|
|
100
|
+
getDocumentPageCount,
|
|
101
|
+
getPDFPageCount,
|
|
102
|
+
getPageCountMetadata,
|
|
103
|
+
getTotalPageCount,
|
|
104
|
+
splitPDFIntoChunks
|
|
105
|
+
};
|
|
106
|
+
//# sourceMappingURL=pdf-utils.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/pdf-utils.ts","../src/runtime/base64.ts"],"sourcesContent":["/**\n * PDF Utilities\n *\n * Edge Runtime compatible PDF manipulation utilities using pdf-lib.\n * These functions work in Node.js, Vercel Edge Functions, Cloudflare Workers, and browsers.\n */\n\nimport { PDFDocument } from 'pdf-lib';\nimport { base64ToArrayBuffer, uint8ArrayToBase64 } from './runtime/base64.js';\nimport type { DocumentIR } from './internal/validation-utils.js';\n\n/**\n * Get the total number of pages in a PDF document\n *\n * @param dataUrl - PDF data URI in format: data:application/pdf;base64,{base64data}\n * @returns Total page count\n * @throws {Error} If the input is not a valid PDF data URL\n *\n * @example\n * ```typescript\n * const pageCount = await getPDFPageCount('data:application/pdf;base64,JVBERi0...');\n * console.log(`PDF has ${pageCount} pages`);\n * ```\n */\nexport async function getPDFPageCount(dataUrl: string): Promise<number> {\n const base64Match = dataUrl.match(/^data:application\\/pdf;base64,(.+)$/);\n if (!base64Match) {\n throw new Error('Invalid PDF data URL format. Expected: data:application/pdf;base64,{base64data}');\n }\n\n const base64Data = base64Match[1];\n const pdfBytes = base64ToArrayBuffer(base64Data);\n const pdfDoc = await PDFDocument.load(pdfBytes);\n return pdfDoc.getPageCount();\n}\n\n/**\n * Split a PDF into multiple smaller PDFs based on page ranges\n *\n * @param dataUrl - PDF data URI in format: data:application/pdf;base64,{base64data}\n * @param pageRanges - Array of [startPage, endPage] tuples (1-indexed, inclusive)\n * @returns Array of PDF data URLs, one for each page range\n * @throws {Error} If the input is not a valid PDF data URL or page ranges are invalid\n *\n * @example\n * ```typescript\n * // Split a 10-page PDF into three chunks\n * const chunks = await splitPDFIntoChunks(pdfDataUrl, [\n * [1, 3], // Pages 1-3\n * [4, 7], // Pages 4-7\n * [8, 10] // Pages 8-10\n * ]);\n * console.log(`Created ${chunks.length} PDF chunks`);\n * ```\n */\nexport async function splitPDFIntoChunks(\n dataUrl: string,\n pageRanges: Array<[number, number]>\n): Promise<string[]> {\n // Extract base64 data from data URL\n const base64Match = dataUrl.match(/^data:application\\/pdf;base64,(.+)$/);\n if (!base64Match) {\n throw new Error('Invalid PDF data URL format. Expected: data:application/pdf;base64,{base64data}');\n }\n\n const base64Data = base64Match[1];\n const pdfBytes = base64ToArrayBuffer(base64Data);\n\n // Load the PDF\n const pdfDoc = await PDFDocument.load(pdfBytes);\n const totalPages = pdfDoc.getPageCount();\n\n const chunks: string[] = [];\n\n for (const [startPage, endPage] of pageRanges) {\n // Validate page range\n if (startPage < 1 || endPage > totalPages || startPage > endPage) {\n throw new Error(\n `Invalid page range [${startPage}, ${endPage}] for PDF with ${totalPages} pages. ` +\n `Page numbers must be 1-indexed and within bounds.`\n );\n }\n\n // Create new PDF with only these pages\n const chunkDoc = await PDFDocument.create();\n const pagesToCopy = Array.from(\n { length: endPage - startPage + 1 },\n (_, i) => startPage - 1 + i // Convert to 0-indexed\n );\n\n const copiedPages = await chunkDoc.copyPages(pdfDoc, pagesToCopy);\n copiedPages.forEach(page => chunkDoc.addPage(page));\n\n // Serialize to base64 using Edge Runtime compatible adapter\n const chunkBytes = await chunkDoc.save();\n const chunkBase64 = uint8ArrayToBase64(chunkBytes);\n chunks.push(`data:application/pdf;base64,${chunkBase64}`);\n }\n\n return chunks;\n}\n\n/**\n * Get the page count from a DocumentIR, with fallback logic\n *\n * This helper function checks multiple sources for page count:\n * 1. `extras.pageCount` (explicit page count from provider or PDF analysis)\n * 2. `pages.length` (fallback - number of pages in the IR)\n *\n * Note: For Unsiloed provider, `pages.length` represents semantic chunks,\n * not traditional pages. Use `extras.totalSemanticChunks` to distinguish.\n *\n * @param ir - DocumentIR to get page count from\n * @returns Page count (or chunk count for Unsiloed)\n *\n * @example\n * ```typescript\n * const ir = await parseNode.run(pdfUrl, { provider: ocrProvider });\n * const pageCount = getDocumentPageCount(ir);\n * console.log(`Document has ${pageCount} pages`);\n * ```\n */\nexport function getDocumentPageCount(ir: DocumentIR): number {\n // Prefer explicit pageCount from extras\n if (ir.extras?.pageCount !== undefined) {\n return ir.extras.pageCount;\n }\n\n // Fallback to pages array length\n return ir.pages.length;\n}\n\n/**\n * Get total page count across multiple DocumentIR objects (chunked results)\n *\n * For chunked parsing results, this sums up the page counts across all chunks.\n * It respects `extras.pageCount` if available, otherwise uses `pages.length`.\n *\n * @param irArray - Array of DocumentIR objects from chunked parsing\n * @returns Total page count across all chunks\n *\n * @example\n * ```typescript\n * const chunks = await parseNode.run(largePdfUrl, {\n * provider: ocrProvider,\n * chunked: { maxPagesPerChunk: 10 }\n * });\n * const totalPages = getTotalPageCount(chunks);\n * console.log(`Total pages across ${chunks.length} chunks: ${totalPages}`);\n * ```\n */\nexport function getTotalPageCount(irArray: DocumentIR[]): number {\n return irArray.reduce((sum, ir) => sum + getDocumentPageCount(ir), 0);\n}\n\n/**\n * Get comprehensive page-related metadata from a DocumentIR\n *\n * Returns detailed information about page counts, chunk information,\n * and whether the result is chunked or a complete document.\n *\n * @param ir - DocumentIR to analyze\n * @returns Metadata object with page count details\n *\n * @example\n * ```typescript\n * const metadata = getPageCountMetadata(ir);\n * console.log(`Document has ${metadata.pageCount} pages`);\n * if (metadata.isChunked) {\n * console.log(`This is chunk ${metadata.chunkIndex + 1} of ${metadata.totalChunks}`);\n * console.log(`Contains pages ${metadata.pageRange[0]} to ${metadata.pageRange[1]}`);\n * }\n * ```\n */\nexport function getPageCountMetadata(ir: DocumentIR): {\n /** Total page count (or chunk count for Unsiloed) */\n pageCount: number;\n /** Number of pages in the IR (may differ from pageCount for chunked docs) */\n pagesInIR: number;\n /** Whether this is a chunked result */\n isChunked: boolean;\n /** For chunked results: which chunk this is (0-indexed) */\n chunkIndex?: number;\n /** For chunked results: total number of chunks */\n totalChunks?: number;\n /** For chunked results: page range [start, end] (1-indexed, inclusive) */\n pageRange?: [number, number];\n /** For Unsiloed: total semantic chunks */\n totalSemanticChunks?: number;\n /** Whether this is from Unsiloed (semantic chunking, not traditional pages) */\n isSemanticChunking: boolean;\n} {\n const pagesInIR = ir.pages.length;\n const pageCount = ir.extras?.pageCount ?? pagesInIR;\n const isSemanticChunking = ir.extras?.totalSemanticChunks !== undefined;\n const isChunked = ir.extras?.chunkIndex !== undefined && ir.extras?.totalChunks !== undefined;\n\n return {\n pageCount,\n pagesInIR,\n isChunked,\n chunkIndex: ir.extras?.chunkIndex as number | undefined,\n totalChunks: ir.extras?.totalChunks as number | undefined,\n pageRange: ir.extras?.pageRange as [number, number] | undefined,\n totalSemanticChunks: ir.extras?.totalSemanticChunks as number | undefined,\n isSemanticChunking\n };\n}\n","/**\n * Universal Base64 Adapter\n *\n * Provides base64 encoding/decoding for both Node.js and Edge Runtime.\n * Replaces Node.js Buffer usage with Web APIs for Edge compatibility.\n *\n * @module @doclo/core/runtime/base64\n */\n\n/**\n * Convert ArrayBuffer to base64 string\n *\n * Uses different strategies depending on runtime:\n * - Edge Runtime / Browser: btoa() with binary string conversion\n * - Node.js: Buffer.toString('base64')\n *\n * @param buffer - ArrayBuffer to encode\n * @returns Base64 encoded string\n *\n * @example\n * ```typescript\n * const buffer = new Uint8Array([72, 101, 108, 108, 111]).buffer;\n * const base64 = arrayBufferToBase64(buffer); // \"SGVsbG8=\"\n * ```\n */\nexport function arrayBufferToBase64(buffer: ArrayBuffer): string {\n // Node.js: Use Buffer for best performance\n if (typeof Buffer !== 'undefined') {\n return Buffer.from(buffer).toString('base64');\n }\n\n // Edge Runtime / Browser: Use btoa() with binary string\n const bytes = new Uint8Array(buffer);\n let binary = '';\n for (let i = 0; i < bytes.byteLength; i++) {\n binary += String.fromCharCode(bytes[i]);\n }\n return btoa(binary);\n}\n\n/**\n * Convert base64 string to ArrayBuffer\n *\n * Uses different strategies depending on runtime:\n * - Edge Runtime / Browser: atob() with Uint8Array conversion\n * - Node.js: Buffer.from(base64, 'base64')\n *\n * @param base64 - Base64 encoded string (with or without data URI prefix)\n * @returns Decoded ArrayBuffer\n *\n * @example\n * ```typescript\n * const buffer = base64ToArrayBuffer(\"SGVsbG8=\");\n * const text = new TextDecoder().decode(buffer); // \"Hello\"\n *\n * // Also handles data URIs\n * const buffer2 = base64ToArrayBuffer(\"data:image/png;base64,iVBORw0KG...\");\n * ```\n */\nexport function base64ToArrayBuffer(base64: string): ArrayBuffer {\n // Remove data URI prefix if present\n const cleanBase64 = base64.replace(/^data:[^;]+;base64,/, '');\n\n // Node.js: Use Buffer for best performance\n if (typeof Buffer !== 'undefined') {\n const buffer = Buffer.from(cleanBase64, 'base64');\n // Convert Node.js Buffer to ArrayBuffer\n return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);\n }\n\n // Edge Runtime / Browser: Use atob()\n const binaryString = atob(cleanBase64);\n const bytes = new Uint8Array(binaryString.length);\n for (let i = 0; i < binaryString.length; i++) {\n bytes[i] = binaryString.charCodeAt(i);\n }\n return bytes.buffer;\n}\n\n/**\n * Convert Uint8Array to base64 string\n *\n * Convenience wrapper around arrayBufferToBase64 for Uint8Array inputs.\n *\n * @param bytes - Uint8Array to encode\n * @returns Base64 encoded string\n */\nexport function uint8ArrayToBase64(bytes: Uint8Array): string {\n return arrayBufferToBase64(bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength) as ArrayBuffer);\n}\n\n/**\n * Convert base64 string to Uint8Array\n *\n * Convenience wrapper around base64ToArrayBuffer with Uint8Array result.\n *\n * @param base64 - Base64 encoded string\n * @returns Decoded Uint8Array\n */\nexport function base64ToUint8Array(base64: string): Uint8Array {\n return new Uint8Array(base64ToArrayBuffer(base64));\n}\n\n/**\n * Create a data URI from ArrayBuffer\n *\n * @param buffer - Data to encode\n * @param mimeType - MIME type (default: application/octet-stream)\n * @returns Data URI string\n *\n * @example\n * ```typescript\n * const buffer = new TextEncoder().encode(\"Hello, World!\");\n * const dataUri = createDataUri(buffer.buffer, 'text/plain');\n * // \"data:text/plain;base64,SGVsbG8sIFdvcmxkIQ==\"\n * ```\n */\nexport function createDataUri(buffer: ArrayBuffer, mimeType = 'application/octet-stream'): string {\n const base64 = arrayBufferToBase64(buffer);\n return `data:${mimeType};base64,${base64}`;\n}\n\n/**\n * Check if a string is a valid data URI\n *\n * @param input - String to check\n * @returns True if valid data URI format\n */\nexport function isDataUri(input: string): boolean {\n return /^data:[^;,]+;base64,/.test(input);\n}\n\n/**\n * Extract MIME type from data URI\n *\n * @param dataUri - Data URI string\n * @returns MIME type or null if invalid\n *\n * @example\n * ```typescript\n * const mime = extractMimeType(\"data:image/png;base64,iVBOR...\");\n * console.log(mime); // \"image/png\"\n * ```\n */\nexport function extractMimeType(dataUri: string): string | null {\n const match = dataUri.match(/^data:([^;,]+);base64,/);\n return match ? match[1] : null;\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Universal Base64 Adapter
|
|
3
|
+
*
|
|
4
|
+
* Provides base64 encoding/decoding for both Node.js and Edge Runtime.
|
|
5
|
+
* Replaces Node.js Buffer usage with Web APIs for Edge compatibility.
|
|
6
|
+
*
|
|
7
|
+
* @module @doclo/core/runtime/base64
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Convert ArrayBuffer to base64 string
|
|
11
|
+
*
|
|
12
|
+
* Uses different strategies depending on runtime:
|
|
13
|
+
* - Edge Runtime / Browser: btoa() with binary string conversion
|
|
14
|
+
* - Node.js: Buffer.toString('base64')
|
|
15
|
+
*
|
|
16
|
+
* @param buffer - ArrayBuffer to encode
|
|
17
|
+
* @returns Base64 encoded string
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```typescript
|
|
21
|
+
* const buffer = new Uint8Array([72, 101, 108, 108, 111]).buffer;
|
|
22
|
+
* const base64 = arrayBufferToBase64(buffer); // "SGVsbG8="
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
declare function arrayBufferToBase64(buffer: ArrayBuffer): string;
|
|
26
|
+
/**
|
|
27
|
+
* Convert base64 string to ArrayBuffer
|
|
28
|
+
*
|
|
29
|
+
* Uses different strategies depending on runtime:
|
|
30
|
+
* - Edge Runtime / Browser: atob() with Uint8Array conversion
|
|
31
|
+
* - Node.js: Buffer.from(base64, 'base64')
|
|
32
|
+
*
|
|
33
|
+
* @param base64 - Base64 encoded string (with or without data URI prefix)
|
|
34
|
+
* @returns Decoded ArrayBuffer
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* ```typescript
|
|
38
|
+
* const buffer = base64ToArrayBuffer("SGVsbG8=");
|
|
39
|
+
* const text = new TextDecoder().decode(buffer); // "Hello"
|
|
40
|
+
*
|
|
41
|
+
* // Also handles data URIs
|
|
42
|
+
* const buffer2 = base64ToArrayBuffer("data:image/png;base64,iVBORw0KG...");
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
45
|
+
declare function base64ToArrayBuffer(base64: string): ArrayBuffer;
|
|
46
|
+
/**
|
|
47
|
+
* Convert Uint8Array to base64 string
|
|
48
|
+
*
|
|
49
|
+
* Convenience wrapper around arrayBufferToBase64 for Uint8Array inputs.
|
|
50
|
+
*
|
|
51
|
+
* @param bytes - Uint8Array to encode
|
|
52
|
+
* @returns Base64 encoded string
|
|
53
|
+
*/
|
|
54
|
+
declare function uint8ArrayToBase64(bytes: Uint8Array): string;
|
|
55
|
+
/**
|
|
56
|
+
* Convert base64 string to Uint8Array
|
|
57
|
+
*
|
|
58
|
+
* Convenience wrapper around base64ToArrayBuffer with Uint8Array result.
|
|
59
|
+
*
|
|
60
|
+
* @param base64 - Base64 encoded string
|
|
61
|
+
* @returns Decoded Uint8Array
|
|
62
|
+
*/
|
|
63
|
+
declare function base64ToUint8Array(base64: string): Uint8Array;
|
|
64
|
+
/**
|
|
65
|
+
* Create a data URI from ArrayBuffer
|
|
66
|
+
*
|
|
67
|
+
* @param buffer - Data to encode
|
|
68
|
+
* @param mimeType - MIME type (default: application/octet-stream)
|
|
69
|
+
* @returns Data URI string
|
|
70
|
+
*
|
|
71
|
+
* @example
|
|
72
|
+
* ```typescript
|
|
73
|
+
* const buffer = new TextEncoder().encode("Hello, World!");
|
|
74
|
+
* const dataUri = createDataUri(buffer.buffer, 'text/plain');
|
|
75
|
+
* // "data:text/plain;base64,SGVsbG8sIFdvcmxkIQ=="
|
|
76
|
+
* ```
|
|
77
|
+
*/
|
|
78
|
+
declare function createDataUri(buffer: ArrayBuffer, mimeType?: string): string;
|
|
79
|
+
/**
|
|
80
|
+
* Check if a string is a valid data URI
|
|
81
|
+
*
|
|
82
|
+
* @param input - String to check
|
|
83
|
+
* @returns True if valid data URI format
|
|
84
|
+
*/
|
|
85
|
+
declare function isDataUri(input: string): boolean;
|
|
86
|
+
/**
|
|
87
|
+
* Extract MIME type from data URI
|
|
88
|
+
*
|
|
89
|
+
* @param dataUri - Data URI string
|
|
90
|
+
* @returns MIME type or null if invalid
|
|
91
|
+
*
|
|
92
|
+
* @example
|
|
93
|
+
* ```typescript
|
|
94
|
+
* const mime = extractMimeType("data:image/png;base64,iVBOR...");
|
|
95
|
+
* console.log(mime); // "image/png"
|
|
96
|
+
* ```
|
|
97
|
+
*/
|
|
98
|
+
declare function extractMimeType(dataUri: string): string | null;
|
|
99
|
+
|
|
100
|
+
export { arrayBufferToBase64, base64ToArrayBuffer, base64ToUint8Array, createDataUri, extractMimeType, isDataUri, uint8ArrayToBase64 };
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// src/runtime/base64.ts
|
|
2
|
+
function arrayBufferToBase64(buffer) {
|
|
3
|
+
if (typeof Buffer !== "undefined") {
|
|
4
|
+
return Buffer.from(buffer).toString("base64");
|
|
5
|
+
}
|
|
6
|
+
const bytes = new Uint8Array(buffer);
|
|
7
|
+
let binary = "";
|
|
8
|
+
for (let i = 0; i < bytes.byteLength; i++) {
|
|
9
|
+
binary += String.fromCharCode(bytes[i]);
|
|
10
|
+
}
|
|
11
|
+
return btoa(binary);
|
|
12
|
+
}
|
|
13
|
+
function base64ToArrayBuffer(base64) {
|
|
14
|
+
const cleanBase64 = base64.replace(/^data:[^;]+;base64,/, "");
|
|
15
|
+
if (typeof Buffer !== "undefined") {
|
|
16
|
+
const buffer = Buffer.from(cleanBase64, "base64");
|
|
17
|
+
return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
|
|
18
|
+
}
|
|
19
|
+
const binaryString = atob(cleanBase64);
|
|
20
|
+
const bytes = new Uint8Array(binaryString.length);
|
|
21
|
+
for (let i = 0; i < binaryString.length; i++) {
|
|
22
|
+
bytes[i] = binaryString.charCodeAt(i);
|
|
23
|
+
}
|
|
24
|
+
return bytes.buffer;
|
|
25
|
+
}
|
|
26
|
+
function uint8ArrayToBase64(bytes) {
|
|
27
|
+
return arrayBufferToBase64(bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength));
|
|
28
|
+
}
|
|
29
|
+
function base64ToUint8Array(base64) {
|
|
30
|
+
return new Uint8Array(base64ToArrayBuffer(base64));
|
|
31
|
+
}
|
|
32
|
+
function createDataUri(buffer, mimeType = "application/octet-stream") {
|
|
33
|
+
const base64 = arrayBufferToBase64(buffer);
|
|
34
|
+
return `data:${mimeType};base64,${base64}`;
|
|
35
|
+
}
|
|
36
|
+
function isDataUri(input) {
|
|
37
|
+
return /^data:[^;,]+;base64,/.test(input);
|
|
38
|
+
}
|
|
39
|
+
function extractMimeType(dataUri) {
|
|
40
|
+
const match = dataUri.match(/^data:([^;,]+);base64,/);
|
|
41
|
+
return match ? match[1] : null;
|
|
42
|
+
}
|
|
43
|
+
export {
|
|
44
|
+
arrayBufferToBase64,
|
|
45
|
+
base64ToArrayBuffer,
|
|
46
|
+
base64ToUint8Array,
|
|
47
|
+
createDataUri,
|
|
48
|
+
extractMimeType,
|
|
49
|
+
isDataUri,
|
|
50
|
+
uint8ArrayToBase64
|
|
51
|
+
};
|
|
52
|
+
//# sourceMappingURL=base64.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/runtime/base64.ts"],"sourcesContent":["/**\n * Universal Base64 Adapter\n *\n * Provides base64 encoding/decoding for both Node.js and Edge Runtime.\n * Replaces Node.js Buffer usage with Web APIs for Edge compatibility.\n *\n * @module @doclo/core/runtime/base64\n */\n\n/**\n * Convert ArrayBuffer to base64 string\n *\n * Uses different strategies depending on runtime:\n * - Edge Runtime / Browser: btoa() with binary string conversion\n * - Node.js: Buffer.toString('base64')\n *\n * @param buffer - ArrayBuffer to encode\n * @returns Base64 encoded string\n *\n * @example\n * ```typescript\n * const buffer = new Uint8Array([72, 101, 108, 108, 111]).buffer;\n * const base64 = arrayBufferToBase64(buffer); // \"SGVsbG8=\"\n * ```\n */\nexport function arrayBufferToBase64(buffer: ArrayBuffer): string {\n // Node.js: Use Buffer for best performance\n if (typeof Buffer !== 'undefined') {\n return Buffer.from(buffer).toString('base64');\n }\n\n // Edge Runtime / Browser: Use btoa() with binary string\n const bytes = new Uint8Array(buffer);\n let binary = '';\n for (let i = 0; i < bytes.byteLength; i++) {\n binary += String.fromCharCode(bytes[i]);\n }\n return btoa(binary);\n}\n\n/**\n * Convert base64 string to ArrayBuffer\n *\n * Uses different strategies depending on runtime:\n * - Edge Runtime / Browser: atob() with Uint8Array conversion\n * - Node.js: Buffer.from(base64, 'base64')\n *\n * @param base64 - Base64 encoded string (with or without data URI prefix)\n * @returns Decoded ArrayBuffer\n *\n * @example\n * ```typescript\n * const buffer = base64ToArrayBuffer(\"SGVsbG8=\");\n * const text = new TextDecoder().decode(buffer); // \"Hello\"\n *\n * // Also handles data URIs\n * const buffer2 = base64ToArrayBuffer(\"data:image/png;base64,iVBORw0KG...\");\n * ```\n */\nexport function base64ToArrayBuffer(base64: string): ArrayBuffer {\n // Remove data URI prefix if present\n const cleanBase64 = base64.replace(/^data:[^;]+;base64,/, '');\n\n // Node.js: Use Buffer for best performance\n if (typeof Buffer !== 'undefined') {\n const buffer = Buffer.from(cleanBase64, 'base64');\n // Convert Node.js Buffer to ArrayBuffer\n return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);\n }\n\n // Edge Runtime / Browser: Use atob()\n const binaryString = atob(cleanBase64);\n const bytes = new Uint8Array(binaryString.length);\n for (let i = 0; i < binaryString.length; i++) {\n bytes[i] = binaryString.charCodeAt(i);\n }\n return bytes.buffer;\n}\n\n/**\n * Convert Uint8Array to base64 string\n *\n * Convenience wrapper around arrayBufferToBase64 for Uint8Array inputs.\n *\n * @param bytes - Uint8Array to encode\n * @returns Base64 encoded string\n */\nexport function uint8ArrayToBase64(bytes: Uint8Array): string {\n return arrayBufferToBase64(bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength) as ArrayBuffer);\n}\n\n/**\n * Convert base64 string to Uint8Array\n *\n * Convenience wrapper around base64ToArrayBuffer with Uint8Array result.\n *\n * @param base64 - Base64 encoded string\n * @returns Decoded Uint8Array\n */\nexport function base64ToUint8Array(base64: string): Uint8Array {\n return new Uint8Array(base64ToArrayBuffer(base64));\n}\n\n/**\n * Create a data URI from ArrayBuffer\n *\n * @param buffer - Data to encode\n * @param mimeType - MIME type (default: application/octet-stream)\n * @returns Data URI string\n *\n * @example\n * ```typescript\n * const buffer = new TextEncoder().encode(\"Hello, World!\");\n * const dataUri = createDataUri(buffer.buffer, 'text/plain');\n * // \"data:text/plain;base64,SGVsbG8sIFdvcmxkIQ==\"\n * ```\n */\nexport function createDataUri(buffer: ArrayBuffer, mimeType = 'application/octet-stream'): string {\n const base64 = arrayBufferToBase64(buffer);\n return `data:${mimeType};base64,${base64}`;\n}\n\n/**\n * Check if a string is a valid data URI\n *\n * @param input - String to check\n * @returns True if valid data URI format\n */\nexport function isDataUri(input: string): boolean {\n return /^data:[^;,]+;base64,/.test(input);\n}\n\n/**\n * Extract MIME type from data URI\n *\n * @param dataUri - Data URI string\n * @returns MIME type or null if invalid\n *\n * @example\n * ```typescript\n * const mime = extractMimeType(\"data:image/png;base64,iVBOR...\");\n * console.log(mime); // \"image/png\"\n * ```\n */\nexport function extractMimeType(dataUri: string): string | null {\n const match = dataUri.match(/^data:([^;,]+);base64,/);\n return match ? match[1] : null;\n}\n"],"mappings":";AAyBO,SAAS,oBAAoB,QAA6B;AAE/D,MAAI,OAAO,WAAW,aAAa;AACjC,WAAO,OAAO,KAAK,MAAM,EAAE,SAAS,QAAQ;AAAA,EAC9C;AAGA,QAAM,QAAQ,IAAI,WAAW,MAAM;AACnC,MAAI,SAAS;AACb,WAAS,IAAI,GAAG,IAAI,MAAM,YAAY,KAAK;AACzC,cAAU,OAAO,aAAa,MAAM,CAAC,CAAC;AAAA,EACxC;AACA,SAAO,KAAK,MAAM;AACpB;AAqBO,SAAS,oBAAoB,QAA6B;AAE/D,QAAM,cAAc,OAAO,QAAQ,uBAAuB,EAAE;AAG5D,MAAI,OAAO,WAAW,aAAa;AACjC,UAAM,SAAS,OAAO,KAAK,aAAa,QAAQ;AAEhD,WAAO,OAAO,OAAO,MAAM,OAAO,YAAY,OAAO,aAAa,OAAO,UAAU;AAAA,EACrF;AAGA,QAAM,eAAe,KAAK,WAAW;AACrC,QAAM,QAAQ,IAAI,WAAW,aAAa,MAAM;AAChD,WAAS,IAAI,GAAG,IAAI,aAAa,QAAQ,KAAK;AAC5C,UAAM,CAAC,IAAI,aAAa,WAAW,CAAC;AAAA,EACtC;AACA,SAAO,MAAM;AACf;AAUO,SAAS,mBAAmB,OAA2B;AAC5D,SAAO,oBAAoB,MAAM,OAAO,MAAM,MAAM,YAAY,MAAM,aAAa,MAAM,UAAU,CAAgB;AACrH;AAUO,SAAS,mBAAmB,QAA4B;AAC7D,SAAO,IAAI,WAAW,oBAAoB,MAAM,CAAC;AACnD;AAgBO,SAAS,cAAc,QAAqB,WAAW,4BAAoC;AAChG,QAAM,SAAS,oBAAoB,MAAM;AACzC,SAAO,QAAQ,QAAQ,WAAW,MAAM;AAC1C;AAQO,SAAS,UAAU,OAAwB;AAChD,SAAO,uBAAuB,KAAK,KAAK;AAC1C;AAcO,SAAS,gBAAgB,SAAgC;AAC9D,QAAM,QAAQ,QAAQ,MAAM,wBAAwB;AACpD,SAAO,QAAQ,MAAM,CAAC,IAAI;AAC5B;","names":[]}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Universal Crypto Adapter
|
|
3
|
+
*
|
|
4
|
+
* Provides crypto-secure random byte generation for both Node.js and Edge Runtime.
|
|
5
|
+
* Uses Web Crypto API (available in both environments) for maximum compatibility.
|
|
6
|
+
*
|
|
7
|
+
* @module @doclo/core/runtime/crypto
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Generate crypto-secure random bytes
|
|
11
|
+
*
|
|
12
|
+
* Uses Web Crypto API which is available in:
|
|
13
|
+
* - Node.js 18+ (globalThis.crypto)
|
|
14
|
+
* - Browsers (window.crypto)
|
|
15
|
+
* - Cloudflare Workers (crypto)
|
|
16
|
+
* - Vercel Edge Functions (crypto)
|
|
17
|
+
*
|
|
18
|
+
* @param length - Number of random bytes to generate
|
|
19
|
+
* @returns Uint8Array containing random bytes
|
|
20
|
+
*/
|
|
21
|
+
declare function getRandomBytes(length: number): Uint8Array;
|
|
22
|
+
/**
|
|
23
|
+
* Convert Uint8Array to lowercase hex string
|
|
24
|
+
*
|
|
25
|
+
* @param bytes - Byte array to convert
|
|
26
|
+
* @returns Hex string representation
|
|
27
|
+
*/
|
|
28
|
+
declare function bytesToHex(bytes: Uint8Array): string;
|
|
29
|
+
/**
|
|
30
|
+
* Generate random hex string of specified byte length
|
|
31
|
+
*
|
|
32
|
+
* @param byteLength - Number of random bytes (hex string will be 2x this length)
|
|
33
|
+
* @returns Lowercase hex string
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* ```typescript
|
|
37
|
+
* const traceId = randomHex(16); // 32 hex characters
|
|
38
|
+
* const spanId = randomHex(8); // 16 hex characters
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
declare function randomHex(byteLength: number): string;
|
|
42
|
+
/**
|
|
43
|
+
* Generate a UUID v4 string
|
|
44
|
+
*
|
|
45
|
+
* Format: xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx
|
|
46
|
+
*
|
|
47
|
+
* @returns UUID v4 string
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* ```typescript
|
|
51
|
+
* const id = randomUUID(); // "550e8400-e29b-41d4-a716-446655440000"
|
|
52
|
+
* ```
|
|
53
|
+
*/
|
|
54
|
+
declare function randomUUID(): string;
|
|
55
|
+
|
|
56
|
+
export { bytesToHex, getRandomBytes, randomHex, randomUUID };
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// src/runtime/crypto.ts
|
|
2
|
+
function getRandomBytes(length) {
|
|
3
|
+
const bytes = new Uint8Array(length);
|
|
4
|
+
if (typeof globalThis !== "undefined" && globalThis.crypto?.getRandomValues) {
|
|
5
|
+
globalThis.crypto.getRandomValues(bytes);
|
|
6
|
+
return bytes;
|
|
7
|
+
}
|
|
8
|
+
throw new Error(
|
|
9
|
+
"Web Crypto API not available. This SDK requires:\n- Node.js 18.0.0 or later (has globalThis.crypto)\n- Edge Runtime (Vercel, Cloudflare)\n- Modern browsers"
|
|
10
|
+
);
|
|
11
|
+
}
|
|
12
|
+
function bytesToHex(bytes) {
|
|
13
|
+
return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
14
|
+
}
|
|
15
|
+
function randomHex(byteLength) {
|
|
16
|
+
const bytes = getRandomBytes(byteLength);
|
|
17
|
+
return bytesToHex(bytes);
|
|
18
|
+
}
|
|
19
|
+
function randomUUID() {
|
|
20
|
+
if (typeof globalThis !== "undefined" && globalThis.crypto?.randomUUID) {
|
|
21
|
+
return globalThis.crypto.randomUUID();
|
|
22
|
+
}
|
|
23
|
+
const bytes = getRandomBytes(16);
|
|
24
|
+
bytes[6] = bytes[6] & 15 | 64;
|
|
25
|
+
bytes[8] = bytes[8] & 63 | 128;
|
|
26
|
+
const hex = bytesToHex(bytes);
|
|
27
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`;
|
|
28
|
+
}
|
|
29
|
+
export {
|
|
30
|
+
bytesToHex,
|
|
31
|
+
getRandomBytes,
|
|
32
|
+
randomHex,
|
|
33
|
+
randomUUID
|
|
34
|
+
};
|
|
35
|
+
//# sourceMappingURL=crypto.js.map
|