diff --git a/apps/mail/lib/trpc.ts b/apps/mail/lib/trpc.ts new file mode 100644 index 0000000000..abe366138a --- /dev/null +++ b/apps/mail/lib/trpc.ts @@ -0,0 +1,27 @@ +import { createTRPCClient, httpBatchLink } from '@trpc/client'; +import type { AppRouter } from '@zero/server/trpc'; +import superjson from 'superjson'; + +const getUrl = () => import.meta.env.VITE_PUBLIC_BACKEND_URL + '/api/trpc'; + +export const api = createTRPCClient({ + links: [ + httpBatchLink({ + maxItems: 1, + url: getUrl(), + transformer: superjson, + fetch: (url, options) => + fetch(url, { ...options, credentials: 'include' }).then((res) => { + if (typeof window !== 'undefined') { + const currentPath = new URL(window.location.href).pathname; + const redirectPath = res.headers.get('X-Zero-Redirect'); + if (!!redirectPath && redirectPath !== currentPath) { + window.location.href = redirectPath; + res.headers.delete('X-Zero-Redirect'); + } + } + return res; + }), + }), + ], +}); \ No newline at end of file diff --git a/apps/server/package.json b/apps/server/package.json index eefdde724c..78af5f4010 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -31,6 +31,7 @@ "@arcadeai/arcadejs": "1.8.1", "@barkleapp/css-sanitizer": "1.0.0", "@coinbase/cookie-manager": "1.1.8", + "@datadog/datadog-api-client": "1.40.0", "@dub/better-auth": "0.0.3", "@googleapis/gmail": "12.0.0", "@googleapis/people": "3.0.9", diff --git a/apps/server/src/ctx.ts b/apps/server/src/ctx.ts index 681f3b8f67..83ec8e2950 100644 --- a/apps/server/src/ctx.ts +++ b/apps/server/src/ctx.ts @@ -8,6 +8,8 @@ export type HonoVariables = { auth: Auth; sessionUser?: SessionUser; autumn?: Autumn; + traceId?: string; + requestId?: string; }; export type HonoContext = { Variables: HonoVariables; Bindings: ZeroEnv }; diff --git a/apps/server/src/env.ts b/apps/server/src/env.ts index 8fdd37f6f7..4a7b37125e 100644 --- a/apps/server/src/env.ts +++ b/apps/server/src/env.ts @@ -1,5 +1,6 @@ import type { ThinkingMCP, ThreadSyncWorker, WorkflowRunner, ZeroDB, ZeroMCP } from './main'; import type { ShardRegistry, ZeroAgent, ZeroDriver } from './routes/agent'; + import { env as _env } from 'cloudflare:workers'; import type { QueryableHandler } from 'dormroom'; @@ -11,6 +12,7 @@ export type ZeroEnv = { ZERO_MCP: DurableObjectNamespace; THINKING_MCP: DurableObjectNamespace; WORKFLOW_RUNNER: DurableObjectNamespace; + THREAD_SYNC_WORKER: DurableObjectNamespace; SYNC_THREADS_WORKFLOW: Workflow; SYNC_THREADS_COORDINATOR_WORKFLOW: Workflow; @@ -97,6 +99,9 @@ export type ZeroEnv = { OTEL_EXPORTER_OTLP_ENDPOINT?: string; OTEL_EXPORTER_OTLP_HEADERS?: string; OTEL_SERVICE_NAME?: string; + DD_API_KEY: string; + DD_APP_KEY: string; + DD_SITE: string; }; const env = _env as ZeroEnv; diff --git a/apps/server/src/lib/datadog-service.ts b/apps/server/src/lib/datadog-service.ts new file mode 100644 index 0000000000..c7737eeec1 --- /dev/null +++ b/apps/server/src/lib/datadog-service.ts @@ -0,0 +1,206 @@ +import { client, v2 } from '@datadog/datadog-api-client'; +import type { TRPCCallLog } from '../types/logging'; +import type { ZeroEnv } from '../env'; + +export class DatadogService { + private apiInstance: v2.LogsApi; + private apiKey: string; + private appKey: string; + private site: string; + + constructor(env?: ZeroEnv) { + // Runtime validation for required Datadog credentials + if (!env?.DD_API_KEY || env.DD_API_KEY.trim() === '') { + throw new Error('DD_API_KEY environment variable is required and cannot be empty for Datadog service'); + } + + if (!env?.DD_APP_KEY || env.DD_APP_KEY.trim() === '') { + throw new Error('DD_APP_KEY environment variable is required and cannot be empty for Datadog service'); + } + + const configuration = client.createConfiguration({ + authMethods: { + apiKeyAuth: env.DD_API_KEY, + appKeyAuth: env.DD_APP_KEY, + }, + }); + + // Set the site for the configuration (defaults to datadoghq.com if not provided) + const ddSite = env?.DD_SITE || 'datadoghq.com'; + configuration.setServerVariables({ site: ddSite }); + + this.apiInstance = new v2.LogsApi(configuration); + this.apiKey = env.DD_API_KEY; + this.appKey = env.DD_APP_KEY; + this.site = ddSite; + } + + private generateId(): string { + return crypto.randomUUID().replace(/-/g, ''); + } + + // Check if a procedure is logging-related to avoid recursive logging + private isLoggingProcedure(procedure: string): boolean { + const loggingProcedures = [ + 'logging.getSessionStats', + 'logging.clearSession', + 'logging.getSessionState', + 'logging.exportToDatadog', + ]; + return loggingProcedures.includes(procedure); + } + + async logSingleCall(sessionId: string, userId: string, log: TRPCCallLog): Promise { + // Skip logging-related procedures to avoid recursive logging + if (this.isLoggingProcedure(log.procedure)) { + return; + } + + try { + const traceId = this.generateId(); + const spanId = this.generateId(); + + const performanceCategory = log.duration < 100 ? 'fast' : log.duration < 500 ? 'normal' : 'slow'; + const hasError = !!log.error; + const logLevel = hasError ? 'error' : performanceCategory === 'slow' ? 'warn' : 'info'; + + // Parse user agent for device/browser info + const parseUserAgent = (userAgent?: string) => { + if (!userAgent) return {}; + + const browsers = { + chrome: /Chrome\/([0-9.]+)/i, + firefox: /Firefox\/([0-9.]+)/i, + safari: /Safari\/([0-9.]+)/i, + edge: /Edg\/([0-9.]+)/i, + }; + + const os = { + windows: /Windows NT ([0-9.]+)/i, + macos: /Mac OS X ([0-9_.]+)/i, + linux: /Linux/i, + android: /Android ([0-9.]+)/i, + ios: /OS ([0-9_]+)/i, + }; + + const devices = { + mobile: /Mobile|Android|iPhone/i, + tablet: /iPad|Tablet/i, + desktop: /Windows|Mac|Linux/i, + }; + + let browser = 'unknown', browserVersion = '', operatingSystem = 'unknown', osVersion = '', deviceType = 'unknown'; + + // Detect browser + for (const [name, regex] of Object.entries(browsers)) { + const match = userAgent.match(regex); + if (match) { + browser = name; + browserVersion = match[1]; + break; + } + } + + // Detect OS + for (const [name, regex] of Object.entries(os)) { + const match = userAgent.match(regex); + if (match) { + operatingSystem = name; + osVersion = match[1]?.replace(/_/g, '.') || ''; + break; + } + } + + // Detect device type + for (const [type, regex] of Object.entries(devices)) { + if (regex.test(userAgent)) { + deviceType = type; + break; + } + } + + return { + browser, + browser_version: browserVersion, + operating_system: operatingSystem, + os_version: osVersion, + device_type: deviceType, + user_agent: userAgent, + }; + }; + + const deviceInfo = parseUserAgent(log.metadata?.userAgent); + + const logEntry = { + message: `${logLevel.toUpperCase()}: TRPC call: [${log.procedure}] (${log.duration}ms)`, + status: logLevel, + service: 'zero-mail-app', + ddsource: 'trpc-logging', + ddtags: `session:${sessionId},user:${userId},procedure:${log.procedure},duration:${log.duration}ms,has_error:${hasError},performance:${performanceCategory},browser:${deviceInfo.browser},device:${deviceInfo.device_type}`, + hostname: 'cloudflare-worker', + timestamp: log.timestamp, + + // Trace correlation fields + dd: { + trace_id: traceId, + span_id: spanId, + }, + + additionalProperties: { + // Core call data + call_id: log.id, + procedure: log.procedure, + duration: log.duration, + performance_category: performanceCategory, + trpc_method: log.metadata?.method || 'unknown', + + // Session context + session_id: sessionId, + user_id: userId, + + // HTTP context + http_method: 'POST', + http_url: `/api/trpc/${log.procedure}`, + client_ip: log.metadata?.ip, + referer: log.metadata?.referer, + origin: log.metadata?.origin, + accept_language: log.metadata?.acceptLanguage, + accept_encoding: log.metadata?.acceptEncoding, + request_id: log.metadata?.requestId, + + // Device and browser information + ...deviceInfo, + + // Error handling + has_error: hasError, + ...(log.error && { + error_message: log.error, + error_type: 'trpc_error', + }), + + // Full request/response data + request_payload: log.input, + ...(log.output && { + response_payload: log.output, + }), + + // Performance metrics + timing: { + start_time: log.metadata?.startTime || log.timestamp, + end_time: log.metadata?.endTime || (log.timestamp + log.duration), + duration_ms: log.duration, + performance_category: performanceCategory, + }, + + // Complete request trace with all spans (from log.trace) + trace: log.trace, + } + }; + + await this.apiInstance.submitLog({ body: [logEntry] }); + + } catch (error) { + console.error('❌ Failed to log TRPC call to Datadog:', error); + } + } +} \ No newline at end of file diff --git a/apps/server/src/lib/logging-service.ts b/apps/server/src/lib/logging-service.ts new file mode 100644 index 0000000000..b18187be07 --- /dev/null +++ b/apps/server/src/lib/logging-service.ts @@ -0,0 +1,117 @@ +import type { TRPCCallLog, LoggingState, SessionStats } from '../types/logging'; +import { DatadogService } from './datadog-service'; +import type { ZeroEnv } from '../env'; + +// In-memory session storage for stats +// In a production environment, you might want to use a distributed cache like Redis +const sessionStats = new Map(); + +export class LoggingService { + private datadogService: DatadogService; + + constructor(env: ZeroEnv) { + this.datadogService = new DatadogService(env); + } + + async logCall(callData: Omit): Promise { + const log: TRPCCallLog = { + ...callData, + id: crypto.randomUUID(), + timestamp: Date.now(), + }; + + // Immediately export to Datadog + try { + await this.datadogService.logSingleCall( + callData.sessionId, + callData.userId, + log + ); + } catch (error) { + console.error('❌ Failed to log TRPC call to Datadog:', error); + } + + // Update in-memory session stats + this.updateSessionStats(callData.sessionId, callData.userId, log); + } + + private updateSessionStats(sessionId: string, userId: string, log: TRPCCallLog): void { + let currentState = sessionStats.get(sessionId); + + if (!currentState) { + currentState = { + sessionId, + userId, + startedAt: Date.now(), + lastActivity: Date.now(), + totalCalls: 0, + totalErrors: 0, + totalDuration: 0, + }; + } + + currentState.lastActivity = log.timestamp; + currentState.totalCalls++; + currentState.totalDuration += log.duration; + + if (log.error) { + currentState.totalErrors++; + } + + sessionStats.set(sessionId, currentState); + } + + getState(sessionId: string): LoggingState { + let state = sessionStats.get(sessionId); + if (!state) { + // Initialize new state + state = { + sessionId, + userId: '', + startedAt: Date.now(), + lastActivity: Date.now(), + totalCalls: 0, + totalErrors: 0, + totalDuration: 0, + }; + sessionStats.set(sessionId, state); + } + return state; + } + + initializeSession(sessionId: string, userId: string): void { + const state = this.getState(sessionId); + state.userId = userId; + state.sessionId = sessionId; + state.startedAt = Date.now(); + state.lastActivity = Date.now(); + sessionStats.set(sessionId, state); + } + + getSessionStats(sessionId: string): SessionStats { + const state = this.getState(sessionId); + const sessionDuration = Date.now() - state.startedAt; + + return { + totalCalls: state.totalCalls, + totalErrors: state.totalErrors, + totalDuration: state.totalDuration, + averageDuration: state.totalCalls > 0 ? state.totalDuration / state.totalCalls : 0, + errorRate: state.totalCalls > 0 ? (state.totalErrors / state.totalCalls) * 100 : 0, + sessionDuration, + }; + } + + clearSession(sessionId: string): void { + const newState: LoggingState = { + sessionId, + userId: '', + startedAt: Date.now(), + lastActivity: Date.now(), + totalCalls: 0, + totalErrors: 0, + totalDuration: 0, + }; + sessionStats.set(sessionId, newState); + } +} diff --git a/apps/server/src/lib/server-utils.ts b/apps/server/src/lib/server-utils.ts index b5ee8d7b1e..c88991263a 100644 --- a/apps/server/src/lib/server-utils.ts +++ b/apps/server/src/lib/server-utils.ts @@ -30,7 +30,7 @@ class MockExecutionContext implements ExecutionContext { console.error('MockExecutionContext: Error in waitUntil', error); } } - passThroughOnException(): void {} + passThroughOnException(): void { } props: any; } @@ -286,15 +286,15 @@ export const getThread: ( connectionId: string, threadId: string, ) => { - const result = await Effect.runPromise(getThreadEffect(connectionId, threadId)); - if (!result.result) { - throw new Error(`Thread ${threadId} not found`); - } - if (!result.shardId) { - throw new Error(`Thread ${threadId} not found in any shard`); - } - return { result: result.result, shardId: result.shardId }; -}; + const result = await Effect.runPromise(getThreadEffect(connectionId, threadId)); + if (!result.result) { + throw new Error(`Thread ${threadId} not found`); + } + if (!result.shardId) { + throw new Error(`Thread ${threadId} not found in any shard`); + } + return { result: result.result, shardId: result.shardId }; + }; export const modifyThreadLabelsInDB = async ( connectionId: string, @@ -501,7 +501,7 @@ const getCounts = async (connectionId: string): Promise => { export const sendDoState = async (connectionId: string) => { try { const agent = await getZeroSocketAgent(connectionId); - + const cached = await agent.getCachedDoState(); if (cached) { console.log(`[sendDoState] Using cached data for connection ${connectionId}`); @@ -522,9 +522,9 @@ export const sendDoState = async (connectionId: string) => { getCounts(connectionId), ]); const shards = await listShards(registry); - + await agent.setCachedDoState(size, counts, shards.length); - + return agent.broadcastChatMessage({ type: OutgoingMessageType.Do_State, isSyncing: false, @@ -604,6 +604,8 @@ export const verifyToken = async (token: string) => { return !!data; }; + + export const resetConnection = async (connectionId: string) => { const { db, conn } = createDb(env.HYPERDRIVE.connectionString); await db diff --git a/apps/server/src/lib/trace-context.ts b/apps/server/src/lib/trace-context.ts new file mode 100644 index 0000000000..8b63ffc581 --- /dev/null +++ b/apps/server/src/lib/trace-context.ts @@ -0,0 +1,240 @@ +export interface TraceSpan { + id: string; + name: string; + startTime: number; + endTime?: number; + duration?: number; + status: 'started' | 'completed' | 'error'; + metadata?: Record; + error?: string; + tags?: Record; +} + +export interface RequestTrace { + traceId: string; + startTime: number; + endTime?: number; + duration?: number; + spans: TraceSpan[]; + metadata: { + procedure?: string; + userId?: string; + sessionId?: string; + ip?: string; + userAgent?: string; + requestId?: string; + }; +} + +class TraceContextClass { + private traces = new Map(); + private readonly MAX_TRACES = 10000; // Maximum number of traces to keep in memory + private readonly TRACE_TTL_MS = 5 * 60 * 1000; // 5 minutes TTL for uncompleted traces + private cleanupInterval: NodeJS.Timeout; + + constructor() { + // Start periodic cleanup every 2 minutes + this.cleanupInterval = setInterval(() => { + this.performCleanup(); + }, 2 * 60 * 1000); + } + + /** + * Performs cleanup of stale traces based on TTL and enforces max size + */ + private performCleanup(): void { + const now = Date.now(); + const tracesToDelete: string[] = []; + + // Find traces that have exceeded TTL + for (const [traceId, trace] of this.traces) { + const age = now - trace.startTime; + if (age > this.TRACE_TTL_MS) { + tracesToDelete.push(traceId); + } + } + + // Remove stale traces + for (const traceId of tracesToDelete) { + this.traces.delete(traceId); + } + + // If still over max size, remove oldest traces (LRU-style eviction) + if (this.traces.size > this.MAX_TRACES) { + const sortedTraces = Array.from(this.traces.entries()) + .sort(([, a], [, b]) => a.startTime - b.startTime); + + const excessCount = this.traces.size - this.MAX_TRACES; + for (let i = 0; i < excessCount; i++) { + this.traces.delete(sortedTraces[i][0]); + } + } + + // Log cleanup statistics in development + if (tracesToDelete.length > 0 || this.traces.size > this.MAX_TRACES * 0.8) { + console.debug(`Trace cleanup: removed ${tracesToDelete.length} stale traces, ${this.traces.size} traces remaining`); + } + } + + /** + * Get current trace statistics for monitoring + */ + getStats(): { totalTraces: number; oldestTraceAge: number } { + if (this.traces.size === 0) { + return { totalTraces: 0, oldestTraceAge: 0 }; + } + + const now = Date.now(); + let oldestAge = 0; + for (const trace of this.traces.values()) { + const age = now - trace.startTime; + oldestAge = Math.max(oldestAge, age); + } + + return { + totalTraces: this.traces.size, + oldestTraceAge: oldestAge, + }; + } + + createTrace(traceId: string, metadata: RequestTrace['metadata']): RequestTrace { + const existing = this.traces.get(traceId); + if (existing) return existing; + + // Trigger cleanup if we're approaching max capacity + if (this.traces.size >= this.MAX_TRACES * 0.9) { + this.performCleanup(); + } + + const trace: RequestTrace = { + traceId, + startTime: Date.now(), + spans: [], + metadata, + }; + this.traces.set(traceId, trace); + return trace; + } + + getTrace(traceId: string): RequestTrace | undefined { + return this.traces.get(traceId); + } + + addSpan(traceId: string, span: Omit): TraceSpan { + const trace = this.traces.get(traceId); + if (!trace) { + throw new Error(`Trace not found: ${traceId}`); + } + + const fullSpan: TraceSpan = { + id: crypto.randomUUID(), + startTime: Date.now(), + status: 'started', + ...span, + }; + + trace.spans.push(fullSpan); + return fullSpan; + } + + completeSpan(traceId: string, spanId: string, metadata?: Record, error?: string): void { + const trace = this.traces.get(traceId); + if (!trace) return; + + const span = trace.spans.find(s => s.id === spanId); + if (!span) return; + + span.endTime = Date.now(); + span.duration = span.endTime - span.startTime; + span.status = error ? 'error' : 'completed'; + if (error) span.error = error; + if (metadata) { + span.metadata = span.metadata ? { ...span.metadata, ...metadata } : metadata; + } + } + + completeTrace(traceId: string): RequestTrace | undefined { + const trace = this.traces.get(traceId); + if (!trace) return; + + trace.endTime = Date.now(); + trace.duration = trace.endTime - trace.startTime; + + setTimeout(() => { + this.traces.delete(traceId); + }, 10000); + + return trace; + } + + destroy(): void { + if (this.cleanupInterval) { + clearInterval(this.cleanupInterval); + } + this.traces.clear(); + } + + // Helper to create and immediately start a span + startSpan(traceId: string, name: string, metadata?: Record, tags?: Record): TraceSpan { + return this.addSpan(traceId, { + name, + metadata, + tags, + }); + } +} + +export const TraceContext = new TraceContextClass(); + +// Helper function to safely get trace from request context using context variables +export function getRequestTrace(c: any): RequestTrace | undefined { + // Try to get trace ID from context variables (set in main.ts) + const traceId = c?.var?.traceId || c?.get?.('traceId'); + + // Fallback to headers if context variables aren't available + if (!traceId) { + const headerTraceId = c.req?.header?.('X-Trace-ID') || + c.req?.header?.('x-trace-id') || + c.req?.headers?.get?.('X-Trace-ID') || + c.req?.headers?.get?.('x-trace-id'); + + if (!headerTraceId) { + return undefined; + } + + return TraceContext.getTrace(headerTraceId); + } + + return TraceContext.getTrace(traceId); +} + +// Helper function to get trace ID from context variables or headers +export function getTraceId(c: any): string | undefined { + return c?.var?.traceId || c?.get?.('traceId') || c.req?.header?.('X-Trace-ID') || c.req?.header?.('x-trace-id'); +} + +// Helper function to safely add span to current request +export function addRequestSpan(c: any, name: string, metadata?: Record, tags?: Record): TraceSpan | undefined { + const traceId = getTraceId(c); + if (!traceId) return undefined; + + return TraceContext.startSpan(traceId, name, metadata, tags); +} + +// Helper function to complete span in current request +export function completeRequestSpan(c: any, spanId: string, metadata?: Record, error?: string): void { + const traceId = getTraceId(c); + if (!traceId) return; + + TraceContext.completeSpan(traceId, spanId, metadata, error); +} + +// Helper function to get trace context statistics for monitoring +export function getTraceStats(): { totalTraces: number; oldestTraceAge: number } { + return TraceContext.getStats(); +} + +// Helper function for graceful shutdown +export function destroyTraceContext(): void { + TraceContext.destroy(); +} \ No newline at end of file diff --git a/apps/server/src/lib/trpc-logging.ts b/apps/server/src/lib/trpc-logging.ts new file mode 100644 index 0000000000..3dfd7db28d --- /dev/null +++ b/apps/server/src/lib/trpc-logging.ts @@ -0,0 +1,245 @@ +import type { TRPCCallLog } from '../types/logging'; +import { LoggingService } from './logging-service'; +import { getContext } from 'hono/context-storage'; +import type { HonoContext } from '../ctx'; + +// Utility function to hash IP addresses for PII protection +function hashIpAddress(ip: string | undefined): string | undefined { + if (!ip) return undefined; + + // Simple but effective hash for IP addresses + // This preserves uniqueness while protecting PII + const salt = 'zero-mail-ip-salt-2024'; // Consider using env variable for production + let hash = 0; + const str = ip + salt; + + for (let i = 0; i < str.length; i++) { + const char = str.charCodeAt(i); + hash = ((hash << 5) - hash) + char; + hash = hash & hash; // Convert to 32bit integer + } + + // Return a prefixed hex representation + return `ip_${Math.abs(hash).toString(16).padStart(8, '0')}`; +} + +export interface LoggingContext { + sessionId: string; + userId?: string; +} + +export const createLoggingMiddleware = () => { + return async (opts: { + path: string; + type: 'query' | 'mutation' | 'subscription'; + next: () => Promise; + input: any; + ctx: any; + }) => { + + const startTime = Date.now(); + const c = getContext(); + const sessionId = c.var.sessionUser?.id || 'anonymous'; + const userId = c.var.sessionUser?.id; + + // Initialize logging service + let loggingService: LoggingService | undefined; + if (userId && c.env) { + try { + loggingService = new LoggingService(c.env); + loggingService.initializeSession(sessionId, userId); + } catch (error) { + console.error('Failed to initialize logging service:', error); + } + } + + let output: any; + let error: string | undefined; + + // Start TRPC procedure execution span + const { addRequestSpan, completeRequestSpan } = await import('./trace-context'); + const procedureSpan = addRequestSpan(c, 'trpc_procedure_execution', { + procedure: opts.path, + type: opts.type, + hasInput: !!opts.input, + inputSize: opts.input ? JSON.stringify(opts.input).length : 0, + }, { + 'trpc.procedure': opts.path, + 'trpc.type': opts.type, + }); + + try { + // Execute the TRPC call + output = await opts.next(); + + // Complete procedure span + if (procedureSpan) { + completeRequestSpan(c, procedureSpan.id, { + success: true, + hasOutput: !!output, + outputSize: output ? JSON.stringify(output).length : 0, + }); + } + + // Sanitize output to remove non-serializable objects + const sanitizeOutput = (obj: any): any => { + if (obj === null || obj === undefined) return obj; + if (typeof obj !== 'object') return obj; + if (Array.isArray(obj)) return obj.map(sanitizeOutput); + + const sanitized: any = {}; + for (const [key, value] of Object.entries(obj)) { + // Skip known non-serializable fields + if (key === 'ctx' && value && typeof value === 'object') { + continue; + } + + try { + structuredClone(value); + sanitized[key] = sanitizeOutput(value); + } catch (err) { + // If it can't be serialized, replace with a description + console.log('[TRACE DEBUG] Non-serializable value:', err); + sanitized[key] = `[Non-serializable: ${value?.constructor?.name || typeof value}]`; + } + } + return sanitized; + }; + + // Log successful call + const callData: TRPCCallLog = { + id: crypto.randomUUID(), + timestamp: startTime, + userId: userId || 'anonymous', + sessionId, + procedure: opts.path, + input: opts.input, + output: sanitizeOutput(output), + duration: Date.now() - startTime, + metadata: { + method: opts.type, + userAgent: c.req.header('User-Agent'), + ip: hashIpAddress(c.req.header('CF-Connecting-IP') || c.req.header('X-Forwarded-For')), + referer: c.req.header('Referer'), + origin: c.req.header('Origin'), + acceptLanguage: c.req.header('Accept-Language'), + acceptEncoding: c.req.header('Accept-Encoding'), + requestId: c.req.header('X-Request-Id') || crypto.randomUUID(), + timestamp: new Date().toISOString(), + startTime, + endTime: Date.now(), + }, + }; + + // Log using the new logging service + if (loggingService) { + const { getRequestTrace } = await import('./trace-context'); + + // Get the complete trace for this request + const trace = getRequestTrace(c); + + // Add trace to call data + if (trace) { + callData.trace = { + traceId: trace.traceId, + requestStartTime: trace.startTime, + requestEndTime: trace.endTime, + requestDuration: trace.duration, + spans: trace.spans, + totalSpans: trace.spans.length, + completedSpans: trace.spans.filter(s => s.status === 'completed').length, + errorSpans: trace.spans.filter(s => s.status === 'error').length, + }; + callData.metadata.traceId = trace.traceId; + callData.metadata.requestDuration = trace.duration; + } + + // Log using the new service which will immediately log to Datadog + loggingService.logCall(callData).catch((err) => { + console.error('Failed to log TRPC call:', err); + }); + + // Complete the trace after logging + if (trace) { + const { TraceContext } = await import('./trace-context'); + TraceContext.completeTrace(trace.traceId); + } + } + + } catch (err) { + error = err instanceof Error ? err.message : 'Unknown error'; + + // Complete procedure span with error + if (procedureSpan) { + completeRequestSpan(c, procedureSpan.id, { + success: false, + errorType: err instanceof Error ? err.constructor.name : 'UnknownError', + }, error); + } + + // Log failed call + const callData: TRPCCallLog = { + id: crypto.randomUUID(), + timestamp: startTime, + userId: userId || 'anonymous', + sessionId, + procedure: opts.path, + input: opts.input, + error, + duration: Date.now() - startTime, + metadata: { + method: opts.type, + userAgent: c.req.header('User-Agent'), + ip: hashIpAddress(c.req.header('CF-Connecting-IP') || c.req.header('X-Forwarded-For')), + referer: c.req.header('Referer'), + origin: c.req.header('Origin'), + acceptLanguage: c.req.header('Accept-Language'), + acceptEncoding: c.req.header('Accept-Encoding'), + requestId: c.req.header('X-Request-Id') || crypto.randomUUID(), + timestamp: new Date().toISOString(), + startTime, + endTime: Date.now(), + }, + }; + + // Log error using the new logging service + if (loggingService) { + const { getRequestTrace } = await import('./trace-context'); + + // Get the complete trace for this request + const trace = getRequestTrace(c); + + // Add trace to call data + if (trace) { + callData.trace = { + traceId: trace.traceId, + requestStartTime: trace.startTime, + requestEndTime: trace.endTime, + requestDuration: trace.duration, + spans: trace.spans, + totalSpans: trace.spans.length, + completedSpans: trace.spans.filter(s => s.status === 'completed').length, + errorSpans: trace.spans.filter(s => s.status === 'error').length, + }; + callData.metadata.traceId = trace.traceId; + callData.metadata.requestDuration = trace.duration; + } + + // Log using the new service which will immediately log to Datadog + loggingService.logCall(callData).catch((logErr) => { + console.error('Failed to log TRPC error:', logErr); + }); + + // Complete the trace after logging error + if (trace) { + const { TraceContext } = await import('./trace-context'); + TraceContext.completeTrace(trace.traceId); + } + } + + throw err; + } + + return output; + }; +}; \ No newline at end of file diff --git a/apps/server/src/main.ts b/apps/server/src/main.ts index a9bb9f51d7..54d7d2f0d5 100644 --- a/apps/server/src/main.ts +++ b/apps/server/src/main.ts @@ -31,6 +31,7 @@ import { oAuthDiscoveryMetadata } from 'better-auth/plugins'; import { EProviders, type IEmailSendBatch } from './types'; import { eq, and, desc, asc, inArray } from 'drizzle-orm'; import { ThinkingMCP } from './lib/sequential-thinking'; + import { contextStorage } from 'hono/context-storage'; import { defaultUserSettings } from './lib/schemas'; import { createLocalJWKSet, jwtVerify } from 'jose'; @@ -563,32 +564,141 @@ class ZeroDB extends DurableObject { } } +// Utility function to hash IP addresses for PII protection +function hashIpAddress(ip: string | undefined): string | undefined { + if (!ip) return undefined; + + // Simple but effective hash for IP addresses + // This preserves uniqueness while protecting PII + const salt = 'zero-mail-ip-salt-2024'; // Consider using env variable for production + let hash = 0; + const str = ip + salt; + + for (let i = 0; i < str.length; i++) { + const char = str.charCodeAt(i); + hash = ((hash << 5) - hash) + char; + hash = hash & hash; // Convert to 32bit integer + } + + // Return a prefixed hex representation + return `ip_${Math.abs(hash).toString(16).padStart(8, '0')}`; +} + const api = new Hono() .use(contextStorage()) .use('*', async (c, next) => { + // Initialize request tracing using headers (no context pollution) + const traceId = c.req.header('X-Trace-ID') || crypto.randomUUID(); + const requestId = c.req.header('X-Request-Id') || crypto.randomUUID(); + + // Set trace ID in response headers for client correlation + c.header('X-Trace-ID', traceId); + c.header('X-Request-ID', requestId); + + // Store trace ID in context variables for TRPC access + c.set('traceId', traceId); + c.set('requestId', requestId); + + const { TraceContext } = await import('./lib/trace-context'); + + // Create trace for this request + const rawIp = c.req.header('CF-Connecting-IP') || c.req.header('X-Forwarded-For'); + const trace = TraceContext.createTrace(traceId, { + requestId, + ip: hashIpAddress(rawIp), // Hash IP address to protect PII + userAgent: c.req.header('User-Agent'), + }); + + // Start authentication span + const authSpan = TraceContext.startSpan(traceId, 'authentication', { + method: c.req.method, + url: c.req.url, + hasAuthHeader: !!c.req.header('Authorization'), + }, { + 'auth.method': c.req.header('Authorization') ? 'bearer_token' : 'session_cookie' + }); + const auth = createAuth(); c.set('auth', auth); const session = await auth.api.getSession({ headers: c.req.raw.headers }); c.set('sessionUser', session?.user); if (c.req.header('Authorization') && !session?.user) { + // Start token verification span + const tokenSpan = TraceContext.startSpan(traceId, 'token_verification', { + tokenPresent: true, + }, { + 'auth.token_type': 'jwt' + }); + const token = c.req.header('Authorization')?.split(' ')[1]; if (token) { - const localJwks = await auth.api.getJwks(); - const jwks = createLocalJWKSet(localJwks); + try { + const localJwks = await auth.api.getJwks(); + const jwks = createLocalJWKSet(localJwks); - const { payload } = await jwtVerify(token, jwks); - const userId = payload.sub; + const { payload } = await jwtVerify(token, jwks); + const userId = payload.sub; - if (userId) { - const db = await getZeroDB(userId); - c.set('sessionUser', await db.findUser()); + if (userId) { + const db = await getZeroDB(userId); + const user = await db.findUser(); + c.set('sessionUser', user); + + TraceContext.completeSpan(traceId, tokenSpan.id, { + success: true, + userId, + }); + } else { + TraceContext.completeSpan(traceId, tokenSpan.id, { + success: false, + reason: 'no_user_id_in_token', + }); + } + } catch (error) { + TraceContext.completeSpan(traceId, tokenSpan.id, { + success: false, + reason: 'token_verification_failed', + }, error instanceof Error ? error.message : 'Unknown token error'); } + } else { + TraceContext.completeSpan(traceId, tokenSpan.id, { + success: false, + reason: 'no_token_provided', + }); } } - await next(); + // Complete auth span + TraceContext.completeSpan(traceId, authSpan.id, { + authenticated: !!c.var.sessionUser, + userId: c.var.sessionUser?.id, + authMethod: session?.user ? 'session' : (c.req.header('Authorization') ? 'token' : 'none'), + }); + + // Update trace metadata with user info + trace.metadata.userId = c.var.sessionUser?.id; + trace.metadata.sessionId = c.var.sessionUser?.id || 'anonymous'; + + // Start request processing span + const requestSpan = TraceContext.startSpan(traceId, 'request_processing', { + authenticated: !!c.var.sessionUser, + path: new URL(c.req.url).pathname, + }); + + try { + await next(); + // Don't complete the request span here - let TRPC middleware handle it + } catch (error) { + TraceContext.completeSpan(traceId, requestSpan.id, { + success: false, + + statusCode: c.res.status, + }, error instanceof Error ? error.message : 'Unknown request error'); + throw error; + } + // Note: Trace will be completed by TRPC middleware after logging c.set('sessionUser', undefined); c.set('auth', undefined as any); diff --git a/apps/server/src/trpc/index.ts b/apps/server/src/trpc/index.ts index 5a5a433b89..2df9cf237a 100644 --- a/apps/server/src/trpc/index.ts +++ b/apps/server/src/trpc/index.ts @@ -17,6 +17,7 @@ import { bimiRouter } from './routes/bimi'; import type { HonoContext } from '../ctx'; import { aiRouter } from './routes/ai'; import { router } from './trpc'; +import { loggingRouter } from './routes/logging'; export const appRouter = router({ ai: aiRouter, @@ -34,6 +35,7 @@ export const appRouter = router({ user: userRouter, templates: templatesRouter, meet: meetRouter, + logging: loggingRouter, }); export type AppRouter = typeof appRouter; diff --git a/apps/server/src/trpc/routes/logging.ts b/apps/server/src/trpc/routes/logging.ts new file mode 100644 index 0000000000..417d3a970d --- /dev/null +++ b/apps/server/src/trpc/routes/logging.ts @@ -0,0 +1,45 @@ +import { privateProcedure, router } from '../trpc'; +import { LoggingService } from '../../lib/logging-service'; +import { TRPCError } from '@trpc/server'; + +export const loggingRouter = router({ + getSessionStats: privateProcedure + .query(async ({ ctx }) => { + if (!ctx.sessionUser) { + throw new TRPCError({ + code: 'UNAUTHORIZED', + message: 'Authentication required', + }); + } + const sessionId = ctx.sessionUser.id; + const loggingService = new LoggingService(ctx.c.env); + return loggingService.getSessionStats(sessionId); + }), + + clearSession: privateProcedure + .mutation(async ({ ctx }) => { + if (!ctx.sessionUser) { + throw new TRPCError({ + code: 'UNAUTHORIZED', + message: 'Authentication required', + }); + } + const sessionId = ctx.sessionUser.id; + const loggingService = new LoggingService(ctx.c.env); + loggingService.clearSession(sessionId); + return { success: true }; + }), + + getSessionState: privateProcedure + .query(async ({ ctx }) => { + if (!ctx.sessionUser) { + throw new TRPCError({ + code: 'UNAUTHORIZED', + message: 'Authentication required', + }); + } + const sessionId = ctx.sessionUser.id; + const loggingService = new LoggingService(ctx.c.env); + return loggingService.getState(sessionId); + }), +}); diff --git a/apps/server/src/trpc/trpc.ts b/apps/server/src/trpc/trpc.ts index 3bb2e55f3e..6edf75e1fb 100644 --- a/apps/server/src/trpc/trpc.ts +++ b/apps/server/src/trpc/trpc.ts @@ -3,6 +3,7 @@ import { Ratelimit, type RatelimitConfig } from '@upstash/ratelimit'; import type { HonoContext, HonoVariables } from '../ctx'; import { getConnInfo } from 'hono/cloudflare-workers'; import { initTRPC, TRPCError } from '@trpc/server'; +import { createLoggingMiddleware } from '../lib/trpc-logging'; import { redis } from '../lib/services'; import type { Context } from 'hono'; @@ -14,24 +15,75 @@ type TrpcContext = { const t = initTRPC.context().create({ transformer: superjson }); +const loggingMiddleware = createLoggingMiddleware(); + export const router = t.router; -export const publicProcedure = t.procedure; +export const publicProcedure = t.procedure.use(loggingMiddleware); export const privateProcedure = publicProcedure.use(async ({ ctx, next }) => { + const { addRequestSpan, completeRequestSpan } = await import('../lib/trace-context'); + + // Start auth validation span + const authSpan = addRequestSpan(ctx.c, 'trpc_auth_validation', { + hasSessionUser: !!ctx.sessionUser, + procedure: 'private', + }, { + 'trpc.auth_required': 'true' + }); + if (!ctx.sessionUser) { + if (authSpan) { + completeRequestSpan(ctx.c, authSpan.id, { + success: false, + reason: 'no_session_user', + }, 'UNAUTHORIZED: No session user found'); + } + throw new TRPCError({ code: 'UNAUTHORIZED', }); } + if (authSpan) { + completeRequestSpan(ctx.c, authSpan.id, { + success: true, + userId: ctx.sessionUser.id, + }); + } + return next({ ctx: { ...ctx, sessionUser: ctx.sessionUser } }); }); export const activeConnectionProcedure = privateProcedure.use(async ({ ctx, next }) => { + const { addRequestSpan, completeRequestSpan } = await import('../lib/trace-context'); + + // Start connection validation span + const connectionSpan = addRequestSpan(ctx.c, 'trpc_connection_validation', { + userId: ctx.sessionUser.id, + }, { + 'trpc.connection_required': 'true' + }); + try { const activeConnection = await getActiveConnection(); + + if (connectionSpan) { + completeRequestSpan(ctx.c, connectionSpan.id, { + success: true, + connectionId: activeConnection.id, + connectionType: activeConnection.providerId, + }); + } + return next({ ctx: { ...ctx, activeConnection } }); } catch (err) { + if (connectionSpan) { + completeRequestSpan(ctx.c, connectionSpan.id, { + success: false, + reason: 'connection_not_found', + }, err instanceof Error ? err.message : 'Failed to get active connection'); + } + await ctx.c.var.auth.api.signOut({ headers: ctx.c.req.raw.headers }); throw new TRPCError({ code: 'BAD_REQUEST', @@ -69,12 +121,12 @@ export const activeDriverProcedure = activeConnectionProcedure.use(async ({ ctx, accessToken: null, refreshToken: null, }); - if (activeConnection.accessToken) { - ctx.c.header( - 'X-Zero-Redirect', - `/settings/connections?disconnectedConnectionId=${activeConnection.id}`, - ); - } + + ctx.c.header( + 'X-Zero-Redirect', + `/settings/connections?disconnectedConnectionId=${activeConnection.id}`, + ); + throw new TRPCError({ code: 'UNAUTHORIZED', message: 'Connection expired. Please reconnect.', diff --git a/apps/server/src/types/logging.ts b/apps/server/src/types/logging.ts new file mode 100644 index 0000000000..d177ced454 --- /dev/null +++ b/apps/server/src/types/logging.ts @@ -0,0 +1,70 @@ +export interface TraceSpan { + id: string; + name: string; + startTime: number; + endTime?: number; + duration?: number; + status: 'started' | 'completed' | 'error'; + metadata?: Record; + error?: string; + tags?: Record; +} + +export interface TRPCCallLog { + id: string; + timestamp: number; + userId: string; + sessionId: string; + procedure: string; + input: any; + output?: any; + error?: string; + duration: number; + metadata: { + userAgent?: string; + ip?: string; + method: 'query' | 'mutation' | 'subscription'; + // Additional metadata + referer?: string; + origin?: string; + acceptLanguage?: string; + acceptEncoding?: string; + requestId?: string; + timestamp?: string; + startTime?: number; + endTime?: number; + // Trace information + traceId?: string; + requestDuration?: number; + }; + // Complete trace spans for this request + trace?: { + traceId: string; + requestStartTime: number; + requestEndTime?: number; + requestDuration?: number; + spans: TraceSpan[]; + totalSpans: number; + completedSpans: number; + errorSpans: number; + }; +} + +export interface LoggingState { + sessionId: string; + userId: string; + startedAt: number; + lastActivity: number; + totalCalls: number; + totalErrors: number; + totalDuration: number; +} + +export interface SessionStats { + totalCalls: number; + totalErrors: number; + totalDuration: number; + averageDuration: number; + errorRate: number; + sessionDuration: number; +} diff --git a/apps/server/wrangler.jsonc b/apps/server/wrangler.jsonc index 309d984e66..f26210292a 100644 --- a/apps/server/wrangler.jsonc +++ b/apps/server/wrangler.jsonc @@ -58,6 +58,7 @@ "name": "WORKFLOW_RUNNER", "class_name": "WorkflowRunner", }, + { "name": "THREAD_SYNC_WORKER", "class_name": "ThreadSyncWorker", @@ -144,6 +145,7 @@ "tag": "v9", "new_sqlite_classes": ["ShardRegistry"], }, + ], "observability": { @@ -177,6 +179,9 @@ "MEET_AUTH_HEADER": "", "OTEL_EXPORTER_OTLP_ENDPOINT": "https://api.axiom.co/v1/traces", "OTEL_SERVICE_NAME": "zero-email-server-local", + "DD_API_KEY": "", + "DD_APP_KEY": "", + "DD_SITE": "datadoghq.com", }, "kv_namespaces": [ { @@ -282,6 +287,7 @@ "name": "SHARD_REGISTRY", "class_name": "ShardRegistry", }, + ], }, "workflows": [ @@ -370,6 +376,7 @@ "tag": "v10", "new_sqlite_classes": ["ShardRegistry"], }, + ], "observability": { "enabled": true, @@ -393,6 +400,9 @@ "DISABLE_WORKFLOWS": "false", "OTEL_EXPORTER_OTLP_ENDPOINT": "https://api.axiom.co/v1/traces", "OTEL_SERVICE_NAME": "zero-email-server-staging", + "DD_API_KEY": "", + "DD_APP_KEY": "", + "DD_SITE": "datadoghq.com", }, "kv_namespaces": [ { @@ -511,6 +521,7 @@ "name": "SHARD_REGISTRY", "class_name": "ShardRegistry", }, + ], }, "workflows": [ @@ -593,6 +604,7 @@ "tag": "v10", "new_sqlite_classes": ["ShardRegistry"], }, + ], "vars": { "NODE_ENV": "production", @@ -606,6 +618,9 @@ "DISABLE_WORKFLOWS": "true", "OTEL_EXPORTER_OTLP_ENDPOINT": "https://api.axiom.co/v1/traces", "OTEL_SERVICE_NAME": "zero-email-server-production", + "DD_API_KEY": "", + "DD_APP_KEY": "", + "DD_SITE": "datadoghq.com", }, "kv_namespaces": [ { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a30e1ff6b8..eae1df6689 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,7 +20,7 @@ catalogs: version: 0.0.48 better-auth: specifier: ^1.3.4 - version: 1.3.4 + version: 1.3.7 drizzle-kit: specifier: ^0.31.1 version: 0.31.4 @@ -41,7 +41,7 @@ catalogs: version: 5.8.3 wrangler: specifier: ^4.28.1 - version: 4.28.1 + version: 4.30.0 zod: specifier: ^3.25.42 version: 3.25.67 @@ -142,7 +142,7 @@ importers: version: 1.1.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@react-router/dev': specifier: ^7.6.1 - version: 7.6.3(@types/node@22.13.8)(jiti@2.4.2)(lightningcss@1.30.1)(react-router@7.6.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(tsx@4.19.4)(typescript@5.8.3)(vite@6.3.5(@types/node@22.13.8)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.4)(yaml@2.8.0))(wrangler@4.28.1(@cloudflare/workers-types@4.20250628.0))(yaml@2.8.0) + version: 7.6.3(@types/node@22.13.8)(jiti@2.4.2)(lightningcss@1.30.1)(react-router@7.6.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(tsx@4.19.4)(typescript@5.8.3)(vite@6.3.5(@types/node@22.13.8)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.4)(yaml@2.8.0))(wrangler@4.30.0(@cloudflare/workers-types@4.20250628.0))(yaml@2.8.0) '@sentry/react': specifier: 9.40.0 version: 9.40.0(react@19.1.0) @@ -232,7 +232,7 @@ importers: version: 19.1.0-rc.2 better-auth: specifier: 'catalog:' - version: 1.3.4(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + version: 1.3.7(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(zod@3.25.67) canvas-confetti: specifier: 1.9.3 version: 1.9.3 @@ -419,7 +419,7 @@ importers: devDependencies: '@cloudflare/vite-plugin': specifier: ^1.3.1 - version: 1.7.5(rollup@4.44.1)(vite@6.3.5(@types/node@22.13.8)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.4)(yaml@2.8.0))(workerd@1.20250803.0)(wrangler@4.28.1(@cloudflare/workers-types@4.20250628.0)) + version: 1.7.5(rollup@4.44.1)(vite@6.3.5(@types/node@22.13.8)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.4)(yaml@2.8.0))(workerd@1.20250813.0)(wrangler@4.30.0(@cloudflare/workers-types@4.20250628.0)) '@inlang/cli': specifier: ^3.0.0 version: 3.0.12 @@ -485,7 +485,7 @@ importers: version: 5.1.4(typescript@5.8.3)(vite@6.3.5(@types/node@22.13.8)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.4)(yaml@2.8.0)) wrangler: specifier: 'catalog:' - version: 4.28.1(@cloudflare/workers-types@4.20250628.0) + version: 4.30.0(@cloudflare/workers-types@4.20250628.0) apps/server: dependencies: @@ -516,6 +516,9 @@ importers: '@coinbase/cookie-manager': specifier: 1.1.8 version: 1.1.8 + '@datadog/datadog-api-client': + specifier: 1.40.0 + version: 1.40.0 '@dub/better-auth': specifier: 0.0.3 version: 0.0.3 @@ -581,7 +584,7 @@ importers: version: 1.5.1 better-auth: specifier: 'catalog:' - version: 1.3.4(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + version: 1.3.7(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(zod@3.25.67) cheerio: specifier: 1.1.0 version: 1.1.0 @@ -599,7 +602,7 @@ importers: version: 1.0.1 drizzle-orm: specifier: 'catalog:' - version: 0.43.1(@cloudflare/workers-types@4.20250628.0)(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(better-sqlite3@11.10.0)(kysely@0.28.2)(postgres@3.4.5) + version: 0.43.1(@cloudflare/workers-types@4.20250628.0)(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(better-sqlite3@11.10.0)(kysely@0.28.5)(postgres@3.4.5) dub: specifier: 0.64.2 version: 0.64.2(@modelcontextprotocol/sdk@1.15.1)(zod@3.25.67) @@ -677,7 +680,7 @@ importers: version: 11.1.0 wrangler: specifier: 'catalog:' - version: 4.28.1(@cloudflare/workers-types@4.20250628.0) + version: 4.30.0(@cloudflare/workers-types@4.20250628.0) zod: specifier: 'catalog:' version: 3.25.67 @@ -1089,8 +1092,8 @@ packages: '@bcoe/v8-coverage@0.2.3': resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} - '@better-auth/utils@0.2.5': - resolution: {integrity: sha512-uI2+/8h/zVsH8RrYdG8eUErbuGBk16rZKQfz8CjxQOyCE6v7BqFYEbFwvOkvl1KbUdxhqOnXp78+uE5h8qVEgQ==} + '@better-auth/utils@0.2.6': + resolution: {integrity: sha512-3y/vaL5Ox33dBwgJ6ub3OPkVqr6B5xL2kgxNHG8eHZuryLyG/4JSPGqjbdRSgjuy9kALUZYDFl+ORIAxlWMSuA==} '@better-fetch/fetch@1.1.18': resolution: {integrity: sha512-rEFOE1MYIsBmoMJtQbl32PGHHXuG2hDxvEd7rUHE0vCBoFQVSDqaVs9hkZEtHCxRoY+CljXKFCOuJ8uxqw1LcA==} @@ -1129,8 +1132,8 @@ packages: workerd: optional: true - '@cloudflare/unenv-preset@2.6.0': - resolution: {integrity: sha512-h7Txw0WbDuUbrvZwky6+x7ft+U/Gppfn/rWx6IdR+e9gjygozRJnV26Y2TOr3yrIFa6OsZqqR2lN+jWTrakHXg==} + '@cloudflare/unenv-preset@2.6.1': + resolution: {integrity: sha512-48rC6jo9CkSRkImfu5KU4zKyoPJx7b9GTUpZn0Emr6J+jkmrLhwCY3BI10QS+fhOt1NkJNlxIcYrBgvWeCpKOw==} peerDependencies: unenv: 2.0.0-rc.19 workerd: ^1.20250802.0 @@ -1150,8 +1153,8 @@ packages: cpu: [x64] os: [darwin] - '@cloudflare/workerd-darwin-64@1.20250803.0': - resolution: {integrity: sha512-6QciMnJp1p3F1qUiN0LaLfmw7SuZA/gfUBOe8Ft81pw16JYZ3CyiqIKPJvc1SV8jgDx8r+gz/PRi1NwOMt329A==} + '@cloudflare/workerd-darwin-64@1.20250813.0': + resolution: {integrity: sha512-Pka37/jqLy7ZaQlwpBy79A/BLH+qpRPSEX2h/zWND+qRfoCVCCaZQPdknHZO0pcvHPzK8E2Z4j5QI1IafPA5UA==} engines: {node: '>=16'} cpu: [x64] os: [darwin] @@ -1162,8 +1165,8 @@ packages: cpu: [arm64] os: [darwin] - '@cloudflare/workerd-darwin-arm64@1.20250803.0': - resolution: {integrity: sha512-DoIgghDowtqoNhL6OoN/F92SKtrk7mRQKc4YSs/Dst8IwFZq+pCShOlWfB0MXqHKPSoiz5xLSrUKR9H6gQMPvw==} + '@cloudflare/workerd-darwin-arm64@1.20250813.0': + resolution: {integrity: sha512-QnaJbmhcA32+4uZ+or1hXZjdxGqrFUuh6Ye+skEGu3iB/xzq9CmyVyoKoshiUOcWGKndQb7KRo56dq0bVvVLFw==} engines: {node: '>=16'} cpu: [arm64] os: [darwin] @@ -1174,8 +1177,8 @@ packages: cpu: [x64] os: [linux] - '@cloudflare/workerd-linux-64@1.20250803.0': - resolution: {integrity: sha512-mYdz4vNWX3+PoqRjssepVQqgh42IBiSrl+wb7vbh7VVWUVzBnQKtW3G+UFiBF62hohCLexGIEi7L0cFfRlcKSQ==} + '@cloudflare/workerd-linux-64@1.20250813.0': + resolution: {integrity: sha512-6pokgBQmujJsAuqOme2wBX5ol/1YW3d7kV7wp0Y1/tFi46TnmWcEy08B4FD5t2AARQJ68a7XMxIJKWChcaJ9Cg==} engines: {node: '>=16'} cpu: [x64] os: [linux] @@ -1186,8 +1189,8 @@ packages: cpu: [arm64] os: [linux] - '@cloudflare/workerd-linux-arm64@1.20250803.0': - resolution: {integrity: sha512-RmrtUYLRUg6djKU7Z6yebS6YGJVnaDVY6bbXca+2s26vw4ibJDOTPLuBHFQF62Grw3fAfsNbjQh5i14vG2mqUg==} + '@cloudflare/workerd-linux-arm64@1.20250813.0': + resolution: {integrity: sha512-lFwqohi8fkR98OwjHT69sbThx4BJem7vu6N8kqrge7wuKJWrMDNbzOTdyBA8adV9DmE07ELuN2vcbbu8ZjaL2Q==} engines: {node: '>=16'} cpu: [arm64] os: [linux] @@ -1198,8 +1201,8 @@ packages: cpu: [x64] os: [win32] - '@cloudflare/workerd-windows-64@1.20250803.0': - resolution: {integrity: sha512-uLV8gdudz36o9sUaAKbBxxTwZwLFz1KyW7QpBvOo4+r3Ib8yVKXGiySIMWGD7A0urSMrjf3e5LlLcJKgZUOjMA==} + '@cloudflare/workerd-windows-64@1.20250813.0': + resolution: {integrity: sha512-Fs62NvUajtoXb+4W8jaRXzw64Nbmb8X+PbRLZbxUFv68sGhxKPw1nB1YEmNNZ215ma47hTlSdF3UQh4FOmz7NA==} engines: {node: '>=16'} cpu: [x64] os: [win32] @@ -1242,6 +1245,10 @@ packages: resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==} engines: {node: '>=18'} + '@datadog/datadog-api-client@1.40.0': + resolution: {integrity: sha512-b8XxPEy0pnsmKed/4aejWMXkzV2IXolUreK7K7KYGJW/bYPpQLJpwBgv2W+GJTaHWX1YfH1veQnm3CW3Qvs04Q==} + engines: {node: '>=12.0.0'} + '@daybrush/utils@1.13.0': resolution: {integrity: sha512-ALK12C6SQNNHw1enXK+UO8bdyQ+jaWNQ1Af7Z3FNxeAwjYhQT7do+TRE4RASAJ3ObaS2+TJ7TXR3oz2Gzbw0PQ==} @@ -4309,11 +4316,11 @@ packages: engines: {node: '>= 8.0.0'} hasBin: true - '@simplewebauthn/browser@13.1.0': - resolution: {integrity: sha512-WuHZ/PYvyPJ9nxSzgHtOEjogBhwJfC8xzYkPC+rR/+8chl/ft4ngjiK8kSU5HtRJfczupyOh33b25TjYbvwAcg==} + '@simplewebauthn/browser@13.1.2': + resolution: {integrity: sha512-aZnW0KawAM83fSBUgglP5WofbrLbLyr7CoPqYr66Eppm7zO86YX6rrCjRB3hQKPrL7ATvY4FVXlykZ6w6FwYYw==} - '@simplewebauthn/server@13.1.1': - resolution: {integrity: sha512-1hsLpRHfSuMB9ee2aAdh0Htza/X3f4djhYISrggqGe3xopNjOcePiSDkDDoPzDYaaMCrbqGP1H2TYU7bgL9PmA==} + '@simplewebauthn/server@13.1.2': + resolution: {integrity: sha512-VwoDfvLXSCaRiD+xCIuyslU0HLxVggeE5BL06+GbsP2l1fGf5op8e0c3ZtKoi+vSg1q4ikjtAghC23ze2Q3H9g==} engines: {node: '>=20.0.0'} '@sinclair/typebox@0.27.8': @@ -4750,6 +4757,9 @@ packages: '@types/babel__traverse@7.28.0': resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} + '@types/buffer-from@1.1.3': + resolution: {integrity: sha512-2lq4YC9uLUMGHkl2IDtX4tCXSo2+hwMpOJcY1qiIk1kybc31rIlPyM1HCVJhkPFIo75a/pOVxqyvwuf5TpCG/w==} + '@types/canvas-confetti@1.9.0': resolution: {integrity: sha512-aBGj/dULrimR1XDZLtG9JwxX1b4HPRF6CX9Yfwh3NvstZEm1ZL7RBnel4keCPSqs1ANRu1u2Aoz9R+VmtjYuTg==} @@ -4864,6 +4874,9 @@ packages: '@types/node@22.15.29': resolution: {integrity: sha512-LNdjOkUDlU1RZb8e1kOIUpN1qQUlzGkEtbVNo53vbrwDg5om6oduhm4SiUaPW5ASTXhAiP0jInWG8Qx9fVlOeQ==} + '@types/pako@1.0.7': + resolution: {integrity: sha512-YBtzT2ztNF6R/9+UXj2wTGFnC9NklAnASt3sC0h2m1bbH7G6FyBIkt4AN8ThZpNfxUo1b2iMVO0UawiJymEt8A==} + '@types/pg-pool@2.0.6': resolution: {integrity: sha512-TaAUE5rq2VQYxab5Ts7WZhKNmuN78Q6PiFonTDdpbx8a1H0M1vhy3rhiMjl+e2iHmogyMw7jZF4FrE6eJUy5HQ==} @@ -5305,11 +5318,12 @@ packages: base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} - better-auth@1.3.4: - resolution: {integrity: sha512-JbZYam6Cs3Eu5CSoMK120zSshfaKvrCftSo/+v7524H1RvhryQ7UtMbzagBcXj0Digjj8hZtVkkR4tTZD/wK2g==} + better-auth@1.3.7: + resolution: {integrity: sha512-/1fEyx2SGgJQM5ujozDCh9eJksnVkNU/J7Fk/tG5Y390l8nKbrPvqiFlCjlMM+scR+UABJbQzA6An7HT50LHyQ==} peerDependencies: react: ^18.0.0 || ^19.0.0 react-dom: ^18.0.0 || ^19.0.0 + zod: ^3.25.0 || ^4.0.0 peerDependenciesMeta: react: optional: true @@ -5649,6 +5663,9 @@ packages: resolution: {integrity: sha512-nH0a49E/kSVk6BeFgKZy4uUsy6D2A16p120h5bYD9ILBhQu7o2sJFH+WI4R731TSBQ0dB1Ik7inB/dRAB4C8QQ==} engines: {node: '>=18'} + cross-fetch@3.2.0: + resolution: {integrity: sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q==} + cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -6169,6 +6186,9 @@ packages: resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==} engines: {node: '>= 0.4'} + es6-promise@4.2.8: + resolution: {integrity: sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==} + esbuild-register@3.6.0: resolution: {integrity: sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg==} peerDependencies: @@ -7314,9 +7334,9 @@ packages: resolution: {integrity: sha512-FIyV/64EkKhJmjgC0g2hygpBv5RNWVPyNCqSAD7eTCv6eFWNIi4PN1UvdSJGicN/o35bnevgis4Y0UDC0qi8jQ==} engines: {node: '>=14.0.0'} - kysely@0.28.2: - resolution: {integrity: sha512-4YAVLoF0Sf0UTqlhgQMFU9iQECdah7n+13ANkiuVfRvlK+uI0Etbgd7bVP36dKlG+NXWbhGua8vnGt+sdhvT7A==} - engines: {node: '>=18.0.0'} + kysely@0.28.5: + resolution: {integrity: sha512-rlB0I/c6FBDWPcQoDtkxi9zIvpmnV5xoIalfCMSMCa7nuA6VGA3F54TW9mEgX4DVf10sXAWCF5fDbamI/5ZpKA==} + engines: {node: '>=20.0.0'} leac@0.6.0: resolution: {integrity: sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg==} @@ -7451,6 +7471,10 @@ packages: lodash@4.17.21: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + loglevel@1.9.2: + resolution: {integrity: sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg==} + engines: {node: '>= 0.6.0'} + long@5.3.2: resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} @@ -7691,8 +7715,8 @@ packages: engines: {node: '>=18.0.0'} hasBin: true - miniflare@4.20250803.0: - resolution: {integrity: sha512-1tmCLfmMw0SqRBF9PPII9CVLQRzOrO7uIBmSng8BMSmtgs2kos7OeoM0sg6KbR9FrvP/zAniLyZuCAMAjuu4fQ==} + miniflare@4.20250813.1: + resolution: {integrity: sha512-6PyXwR4pZmH9ukO0jR5LmhlFVMktsVVGVcUjD9Lpev5QwnqjTRPEv73cnXCe0+oTbIm5TYnvXsAklaWxQuxstA==} engines: {node: '>=18.0.0'} hasBin: true @@ -8053,6 +8077,9 @@ packages: pako@1.0.11: resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} + pako@2.1.0: + resolution: {integrity: sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==} + parent-module@1.0.1: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} @@ -8450,8 +8477,8 @@ packages: proto-list@1.2.4: resolution: {integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==} - protobufjs@7.5.3: - resolution: {integrity: sha512-sildjKwVqOI2kmFDiXQ6aEB0fjYTafpEvIBs8tOR8qI4spuL9OPROLVu2qZqi/xgCfsHIwVqlaF8JBjWFHnKbw==} + protobufjs@7.5.4: + resolution: {integrity: sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==} engines: {node: '>=12.0.0'} proxy-addr@2.0.7: @@ -9244,8 +9271,8 @@ packages: resolution: {integrity: sha512-5JRxVqC8I8NuOUjzBbvVJAKNM8qoVuH0O77h4WInc/qC2q5IreqKxYwgkga3PfA22OayK2ikceb/B26dztPl+Q==} engines: {node: '>=16'} - supports-color@10.1.0: - resolution: {integrity: sha512-GBuewsPrhJPftT+fqDa9oI/zc5HNsG9nREqwzoSFDOIqf0NggOZbHQj2TE1P1CDJK8ZogFnlZY9hWoUiur7I/A==} + supports-color@10.2.0: + resolution: {integrity: sha512-5eG9FQjEjDbAlI5+kdpdyPIBMRH4GfTVDGREVupaZHmVoppknhM29b/S9BkQz7cathp85BVgRi/As3Siln7e0Q==} engines: {node: '>=18'} supports-color@7.2.0: @@ -9991,20 +10018,20 @@ packages: engines: {node: '>=16'} hasBin: true - workerd@1.20250803.0: - resolution: {integrity: sha512-oYH29mE/wNolPc32NHHQbySaNorj6+KASUtOvQHySxB5mO1NWdGuNv49woxNCF5971UYceGQndY+OLT+24C3wQ==} + workerd@1.20250813.0: + resolution: {integrity: sha512-bDlPGSnb/KESpGFE57cDjgP8mEKDM4WBTd/uGJBsQYCB6Aokk1eK3ivtHoxFx3MfJNo3v6/hJy6KK1b6rw1gvg==} engines: {node: '>=16'} hasBin: true workers-og@0.0.25: resolution: {integrity: sha512-OkTyqCkUCUpGHwMwGmVCMtFPUASf9oBEiCYyOVMBDnUidTQt7AwvDx5EIuCMuQELUGm/tIyvvC8OU/hBsxlBUw==} - wrangler@4.28.1: - resolution: {integrity: sha512-B1w6XS3o1q1Icyx1CyirY5GNyYhucd63Jqml/EYSbB5dgv0VT8ir7L8IkCdbICEa4yYTETIgvTTZqffM6tBulA==} + wrangler@4.30.0: + resolution: {integrity: sha512-NXJUObuXxgG8/ChQ4yXkWLmDQ5ZcO98gyq1yFKYVntJ884C0IpDQrVnAv2RA0ZEz5eB8zal+4OKnr26P3N7ItA==} engines: {node: '>=18.0.0'} hasBin: true peerDependencies: - '@cloudflare/workers-types': ^4.20250803.0 + '@cloudflare/workers-types': ^4.20250813.0 peerDependenciesMeta: '@cloudflare/workers-types': optional: true @@ -10123,9 +10150,6 @@ packages: zod@3.25.67: resolution: {integrity: sha512-idA2YXwpCdqUSKRCACDE6ItZD9TZzy3OZMtpfLoh6oPR47lipysRrJfjzMqFxQ3uJuUPyUeWe1r9vLH33xO/Qw==} - zod@4.0.15: - resolution: {integrity: sha512-2IVHb9h4Mt6+UXkyMs0XbfICUh1eUrlJJAOupBHUhLRnKkruawyDddYRCs0Eizt900ntIMk9/4RksYl+FgSpcQ==} - zustand@4.5.7: resolution: {integrity: sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==} engines: {node: '>=12.7.0'} @@ -10515,9 +10539,8 @@ snapshots: '@bcoe/v8-coverage@0.2.3': {} - '@better-auth/utils@0.2.5': + '@better-auth/utils@0.2.6': dependencies: - typescript: 5.8.3 uncrypto: 0.1.3 '@better-fetch/fetch@1.1.18': {} @@ -10556,21 +10579,21 @@ snapshots: '@cloudflare/playwright@0.0.11': {} - '@cloudflare/unenv-preset@2.3.3(unenv@2.0.0-rc.17)(workerd@1.20250803.0)': + '@cloudflare/unenv-preset@2.3.3(unenv@2.0.0-rc.17)(workerd@1.20250813.0)': dependencies: unenv: 2.0.0-rc.17 optionalDependencies: - workerd: 1.20250803.0 + workerd: 1.20250813.0 - '@cloudflare/unenv-preset@2.6.0(unenv@2.0.0-rc.19)(workerd@1.20250803.0)': + '@cloudflare/unenv-preset@2.6.1(unenv@2.0.0-rc.19)(workerd@1.20250813.0)': dependencies: unenv: 2.0.0-rc.19 optionalDependencies: - workerd: 1.20250803.0 + workerd: 1.20250813.0 - '@cloudflare/vite-plugin@1.7.5(rollup@4.44.1)(vite@6.3.5(@types/node@22.13.8)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.4)(yaml@2.8.0))(workerd@1.20250803.0)(wrangler@4.28.1(@cloudflare/workers-types@4.20250628.0))': + '@cloudflare/vite-plugin@1.7.5(rollup@4.44.1)(vite@6.3.5(@types/node@22.13.8)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.4)(yaml@2.8.0))(workerd@1.20250813.0)(wrangler@4.30.0(@cloudflare/workers-types@4.20250628.0))': dependencies: - '@cloudflare/unenv-preset': 2.3.3(unenv@2.0.0-rc.17)(workerd@1.20250803.0) + '@cloudflare/unenv-preset': 2.3.3(unenv@2.0.0-rc.17)(workerd@1.20250813.0) '@mjackson/node-fetch-server': 0.6.1 '@rollup/plugin-replace': 6.0.2(rollup@4.44.1) get-port: 7.1.0 @@ -10579,7 +10602,7 @@ snapshots: tinyglobby: 0.2.14 unenv: 2.0.0-rc.17 vite: 6.3.5(@types/node@22.13.8)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.4)(yaml@2.8.0) - wrangler: 4.28.1(@cloudflare/workers-types@4.20250628.0) + wrangler: 4.30.0(@cloudflare/workers-types@4.20250628.0) ws: 8.18.0 transitivePeerDependencies: - bufferutil @@ -10590,31 +10613,31 @@ snapshots: '@cloudflare/workerd-darwin-64@1.20250617.0': optional: true - '@cloudflare/workerd-darwin-64@1.20250803.0': + '@cloudflare/workerd-darwin-64@1.20250813.0': optional: true '@cloudflare/workerd-darwin-arm64@1.20250617.0': optional: true - '@cloudflare/workerd-darwin-arm64@1.20250803.0': + '@cloudflare/workerd-darwin-arm64@1.20250813.0': optional: true '@cloudflare/workerd-linux-64@1.20250617.0': optional: true - '@cloudflare/workerd-linux-64@1.20250803.0': + '@cloudflare/workerd-linux-64@1.20250813.0': optional: true '@cloudflare/workerd-linux-arm64@1.20250617.0': optional: true - '@cloudflare/workerd-linux-arm64@1.20250803.0': + '@cloudflare/workerd-linux-arm64@1.20250813.0': optional: true '@cloudflare/workerd-windows-64@1.20250617.0': optional: true - '@cloudflare/workerd-windows-64@1.20250803.0': + '@cloudflare/workerd-windows-64@1.20250813.0': optional: true '@cloudflare/workers-types@4.20250628.0': {} @@ -10647,6 +10670,20 @@ snapshots: '@csstools/css-tokenizer@3.0.4': {} + '@datadog/datadog-api-client@1.40.0': + dependencies: + '@types/buffer-from': 1.1.3 + '@types/node': 22.15.29 + '@types/pako': 1.0.7 + buffer-from: 1.1.2 + cross-fetch: 3.2.0 + es6-promise: 4.2.8 + form-data: 4.0.3 + loglevel: 1.9.2 + pako: 2.1.0 + transitivePeerDependencies: + - encoding + '@daybrush/utils@1.13.0': {} '@dnd-kit/accessibility@3.1.1(react@19.1.0)': @@ -11341,7 +11378,7 @@ snapshots: dependencies: '@jridgewell/set-array': 1.2.1 '@jridgewell/sourcemap-codec': 1.5.0 - '@jridgewell/trace-mapping': 0.3.29 + '@jridgewell/trace-mapping': 0.3.25 '@jridgewell/resolve-uri@3.1.2': {} @@ -11758,7 +11795,7 @@ snapshots: '@opentelemetry/sdk-logs': 0.200.0(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-metrics': 2.0.0(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-trace-base': 2.0.0(@opentelemetry/api@1.9.0) - protobufjs: 7.5.3 + protobufjs: 7.5.4 '@opentelemetry/redis-common@0.36.2': {} @@ -11899,7 +11936,7 @@ snapshots: dependencies: '@poppinss/colors': 4.1.5 '@sindresorhus/is': 7.0.2 - supports-color: 10.1.0 + supports-color: 10.2.0 '@poppinss/exception@1.2.2': {} @@ -13313,7 +13350,7 @@ snapshots: dependencies: react: 19.1.0 - '@react-router/dev@7.6.3(@types/node@22.13.8)(jiti@2.4.2)(lightningcss@1.30.1)(react-router@7.6.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(tsx@4.19.4)(typescript@5.8.3)(vite@6.3.5(@types/node@22.13.8)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.4)(yaml@2.8.0))(wrangler@4.28.1(@cloudflare/workers-types@4.20250628.0))(yaml@2.8.0)': + '@react-router/dev@7.6.3(@types/node@22.13.8)(jiti@2.4.2)(lightningcss@1.30.1)(react-router@7.6.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(tsx@4.19.4)(typescript@5.8.3)(vite@6.3.5(@types/node@22.13.8)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.4)(yaml@2.8.0))(wrangler@4.30.0(@cloudflare/workers-types@4.20250628.0))(yaml@2.8.0)': dependencies: '@babel/core': 7.27.7 '@babel/generator': 7.27.5 @@ -13346,7 +13383,7 @@ snapshots: vite-node: 3.2.4(@types/node@22.13.8)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.19.4)(yaml@2.8.0) optionalDependencies: typescript: 5.8.3 - wrangler: 4.28.1(@cloudflare/workers-types@4.20250628.0) + wrangler: 4.30.0(@cloudflare/workers-types@4.20250628.0) transitivePeerDependencies: - '@types/node' - babel-plugin-macros @@ -13672,9 +13709,9 @@ snapshots: fflate: 0.7.4 string.prototype.codepointat: 0.2.1 - '@simplewebauthn/browser@13.1.0': {} + '@simplewebauthn/browser@13.1.2': {} - '@simplewebauthn/server@13.1.1': + '@simplewebauthn/server@13.1.2': dependencies: '@hexagon/base64': 1.1.28 '@levischuck/tiny-cbor': 0.2.11 @@ -14126,6 +14163,10 @@ snapshots: dependencies: '@babel/types': 7.28.2 + '@types/buffer-from@1.1.3': + dependencies: + '@types/node': 22.15.29 + '@types/canvas-confetti@1.9.0': {} '@types/chai@5.2.2': @@ -14239,6 +14280,8 @@ snapshots: dependencies: undici-types: 6.21.0 + '@types/pako@1.0.7': {} + '@types/pg-pool@2.0.6': dependencies: '@types/pg': 8.6.1 @@ -14815,20 +14858,20 @@ snapshots: base64-js@1.5.1: {} - better-auth@1.3.4(react-dom@19.1.0(react@19.1.0))(react@19.1.0): + better-auth@1.3.7(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(zod@3.25.67): dependencies: - '@better-auth/utils': 0.2.5 + '@better-auth/utils': 0.2.6 '@better-fetch/fetch': 1.1.18 '@noble/ciphers': 0.6.0 '@noble/hashes': 1.8.0 - '@simplewebauthn/browser': 13.1.0 - '@simplewebauthn/server': 13.1.1 + '@simplewebauthn/browser': 13.1.2 + '@simplewebauthn/server': 13.1.2 better-call: 1.0.13 defu: 6.1.4 jose: 5.10.0 - kysely: 0.28.2 + kysely: 0.28.5 nanostores: 0.11.4 - zod: 4.0.15 + zod: 3.25.67 optionalDependencies: react: 19.1.0 react-dom: 19.1.0(react@19.1.0) @@ -15216,6 +15259,12 @@ snapshots: cron-schedule@5.0.4: {} + cross-fetch@3.2.0: + dependencies: + node-fetch: 2.7.0 + transitivePeerDependencies: + - encoding + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -15502,13 +15551,13 @@ snapshots: transitivePeerDependencies: - supports-color - drizzle-orm@0.43.1(@cloudflare/workers-types@4.20250628.0)(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(better-sqlite3@11.10.0)(kysely@0.28.2)(postgres@3.4.5): + drizzle-orm@0.43.1(@cloudflare/workers-types@4.20250628.0)(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(better-sqlite3@11.10.0)(kysely@0.28.5)(postgres@3.4.5): optionalDependencies: '@cloudflare/workers-types': 4.20250628.0 '@opentelemetry/api': 1.9.0 '@types/pg': 8.6.1 better-sqlite3: 11.10.0 - kysely: 0.28.2 + kysely: 0.28.5 postgres: 3.4.5 dub@0.64.2(@modelcontextprotocol/sdk@1.15.1)(zod@3.25.67): @@ -15741,6 +15790,8 @@ snapshots: is-date-object: 1.1.0 is-symbol: 1.1.1 + es6-promise@4.2.8: {} + esbuild-register@3.6.0(esbuild@0.25.5): dependencies: debug: 4.4.1 @@ -17116,7 +17167,7 @@ snapshots: kysely@0.27.6: {} - kysely@0.28.2: {} + kysely@0.28.5: {} leac@0.6.0: {} @@ -17231,6 +17282,8 @@ snapshots: lodash@4.17.21: {} + loglevel@1.9.2: {} + long@5.3.2: {} longest-streak@3.1.0: {} @@ -17595,7 +17648,7 @@ snapshots: - bufferutil - utf-8-validate - miniflare@4.20250803.0: + miniflare@4.20250813.1: dependencies: '@cspotcode/source-map-support': 0.8.1 acorn: 8.14.0 @@ -17605,7 +17658,7 @@ snapshots: sharp: 0.33.5 stoppable: 1.1.0 undici: 7.11.0 - workerd: 1.20250803.0 + workerd: 1.20250813.0 ws: 8.18.0 youch: 4.1.0-beta.10 zod: 3.22.3 @@ -18015,6 +18068,8 @@ snapshots: pako@1.0.11: {} + pako@2.1.0: {} + parent-module@1.0.1: dependencies: callsites: 3.1.0 @@ -18384,7 +18439,7 @@ snapshots: proto-list@1.2.4: {} - protobufjs@7.5.3: + protobufjs@7.5.4: dependencies: '@protobufjs/aspromise': 1.1.2 '@protobufjs/base64': 1.1.2 @@ -19431,7 +19486,7 @@ snapshots: dependencies: copy-anything: 3.0.5 - supports-color@10.1.0: {} + supports-color@10.2.0: {} supports-color@7.2.0: dependencies: @@ -20254,13 +20309,13 @@ snapshots: '@cloudflare/workerd-linux-arm64': 1.20250617.0 '@cloudflare/workerd-windows-64': 1.20250617.0 - workerd@1.20250803.0: + workerd@1.20250813.0: optionalDependencies: - '@cloudflare/workerd-darwin-64': 1.20250803.0 - '@cloudflare/workerd-darwin-arm64': 1.20250803.0 - '@cloudflare/workerd-linux-64': 1.20250803.0 - '@cloudflare/workerd-linux-arm64': 1.20250803.0 - '@cloudflare/workerd-windows-64': 1.20250803.0 + '@cloudflare/workerd-darwin-64': 1.20250813.0 + '@cloudflare/workerd-darwin-arm64': 1.20250813.0 + '@cloudflare/workerd-linux-64': 1.20250813.0 + '@cloudflare/workerd-linux-arm64': 1.20250813.0 + '@cloudflare/workerd-windows-64': 1.20250813.0 workers-og@0.0.25: dependencies: @@ -20269,16 +20324,16 @@ snapshots: satori: 0.10.14 yoga-wasm-web: 0.3.3 - wrangler@4.28.1(@cloudflare/workers-types@4.20250628.0): + wrangler@4.30.0(@cloudflare/workers-types@4.20250628.0): dependencies: '@cloudflare/kv-asset-handler': 0.4.0 - '@cloudflare/unenv-preset': 2.6.0(unenv@2.0.0-rc.19)(workerd@1.20250803.0) + '@cloudflare/unenv-preset': 2.6.1(unenv@2.0.0-rc.19)(workerd@1.20250813.0) blake3-wasm: 2.1.5 esbuild: 0.25.4 - miniflare: 4.20250803.0 + miniflare: 4.20250813.1 path-to-regexp: 6.3.0 unenv: 2.0.0-rc.19 - workerd: 1.20250803.0 + workerd: 1.20250813.0 optionalDependencies: '@cloudflare/workers-types': 4.20250628.0 fsevents: 2.3.3 @@ -20387,8 +20442,6 @@ snapshots: zod@3.25.67: {} - zod@4.0.15: {} - zustand@4.5.7(@types/react@19.0.10)(react@19.1.0): dependencies: use-sync-external-store: 1.5.0(react@19.1.0)