import type { StorageProvider } from '@shade/core'; import { ConfigurationError } from '@shade/core'; /** * Shade SDK configuration. * * All fields have sane defaults except `prekeyServer` which is required * for any networked usage. */ export interface ShadeConfig { /** Required: base URL of the prekey server */ prekeyServer: string; /** * Storage backend. Accepts: * - "memory" — in-memory only (lost on restart, good for tests) * - "sqlite:/path/to/file.db" — SQLite backend * - { type: 'postgres', url: 'postgres://...' } — PostgreSQL backend * - An explicit StorageProvider instance * * Default: "memory" */ storage?: string | StorageProvider | { type: 'postgres'; url: string }; /** * Your address on the prekey server (e.g. "alice@example.com" or "device:abc123"). * If omitted, a random UUID is generated and persisted. */ address?: string; /** * Auto-replenish configuration. When the one-time prekey stock drops * below `min`, the SDK will generate enough to reach `target` and * upload them to the prekey server. * * Default: { min: 5, target: 20, intervalMs: 60_000 } * Pass `false` to disable. */ autoReplenish?: { min: number; target: number; intervalMs: number } | false; /** * Auto identity rotation. Default OFF. * Pass an interval string like '7d' or '30d' to enable periodic rotation. */ autoRotate?: false | '1d' | '7d' | '30d' | '90d'; /** * Optional observer configuration. If provided, starts a Shade Observer * endpoint for the dashboard. */ observer?: { /** Bearer token for the observer. Must be at least 16 chars. */ token: string; /** Port to listen on. If not set, observer is created but not served. */ port?: number; /** Path prefix to mount under (default: "/shade-observer") */ basePath?: string; }; } export interface ResolvedConfig { prekeyServer: string; storage: string | StorageProvider | { type: 'postgres'; url: string }; address?: string; autoReplenish: { min: number; target: number; intervalMs: number } | false; autoRotate: false | '1d' | '7d' | '30d' | '90d'; observer?: { token: string; port?: number; basePath: string; }; } /** Parse and validate a ShadeConfig, resolving defaults and env var overrides */ export function resolveConfig(input: ShadeConfig): ResolvedConfig { if (!input.prekeyServer) { throw new ConfigurationError('prekeyServer is required'); } const autoReplenish = input.autoReplenish === false ? false : { min: input.autoReplenish?.min ?? 5, target: input.autoReplenish?.target ?? 20, intervalMs: input.autoReplenish?.intervalMs ?? 60_000, }; const resolved: ResolvedConfig = { prekeyServer: input.prekeyServer.replace(/\/$/, ''), storage: input.storage ?? 'memory', address: input.address, autoReplenish, autoRotate: input.autoRotate ?? false, }; if (input.observer) { if (!input.observer.token || input.observer.token.length < 16) { throw new ConfigurationError('observer.token must be at least 16 characters'); } resolved.observer = { token: input.observer.token, port: input.observer.port, basePath: input.observer.basePath ?? '/shade-observer', }; } return resolved; } /** Parse a rotation interval string ('7d', '30d', etc.) into milliseconds */ export function parseRotationInterval( interval: '1d' | '7d' | '30d' | '90d', ): number { const map: Record = { '1d': 24 * 60 * 60 * 1000, '7d': 7 * 24 * 60 * 60 * 1000, '30d': 30 * 24 * 60 * 60 * 1000, '90d': 90 * 24 * 60 * 60 * 1000, }; return map[interval]!; }