File size: 4,710 Bytes
391c43e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
/**
 * Sandboxed Database API for Edge Functions
 *
 * Provides a secure, limited interface to the deployment's SQLite database
 * for use within edge functions. Blocks access to system tables and
 * enforces query limits.
 */

import { RuntimeDatabase } from '@/lib/vfs/adapters/runtime-database';
import { DatabaseAPI, DatabaseAPIOptions } from './types';

/**
 * Tables that edge functions cannot access
 * These contain system data, analytics, and function definitions
 */
const FORBIDDEN_TABLES = [
  'sqlite_',        // SQLite internal tables
  '_migrations',    // Migration tracking
  'site_info',      // Site configuration
  'edge_functions', // Function definitions
  'function_logs',  // Execution logs
  'server_functions', // Server function definitions
  'secrets',        // Encrypted secrets
  'pageviews',      // Analytics
  'interactions',   // Analytics
  'sessions',       // Analytics
  'files',          // VFS files
  'file_tree_nodes', // VFS tree
];

/**
 * DDL keywords that modify schema
 */
const DDL_KEYWORDS = ['create', 'drop', 'alter', 'truncate'];

/**
 * Dangerous keywords that should never be allowed
 */
const DANGEROUS_KEYWORDS = ['attach', 'detach', 'vacuum', 'reindex'];

/**
 * Create a sandboxed database API for edge function use
 *
 * @param deploymentDb The deployment's database instance
 * @param options Configuration options
 * @returns A DatabaseAPI that enforces security restrictions
 */
export function createDatabaseAPI(
  deploymentDb: RuntimeDatabase,
  options: DatabaseAPIOptions = {}
): DatabaseAPI {
  let queryCount = 0;
  const maxQueries = options.maxQueries ?? 100;
  const readOnly = options.readOnly ?? false;

  /**
   * Validate SQL query for security
   * Throws an error if the query is not allowed
   */
  const validateSQL = (sql: string): void => {
    const lowerSQL = sql.toLowerCase().trim();

    // Check for dangerous keywords
    for (const keyword of DANGEROUS_KEYWORDS) {
      if (lowerSQL.includes(keyword)) {
        throw new Error(`SQL keyword "${keyword}" is not allowed`);
      }
    }

    // Block access to system tables
    for (const table of FORBIDDEN_TABLES) {
      // Check if the forbidden table name appears in the query
      // Use word boundaries to avoid false positives
      const tableRegex = new RegExp(`\\b${table.replace('_', '_?')}\\w*\\b`, 'i');
      if (tableRegex.test(lowerSQL)) {
        throw new Error(`Access to system table "${table}" is not allowed`);
      }
    }

    // Block DDL in read-only mode
    if (readOnly) {
      for (const keyword of DDL_KEYWORDS) {
        if (lowerSQL.startsWith(keyword)) {
          throw new Error(`DDL statements (${keyword.toUpperCase()}) not allowed in read-only mode`);
        }
      }
    }

    // Check query count limit
    queryCount++;
    if (queryCount > maxQueries) {
      throw new Error(`Query limit exceeded (max ${maxQueries} queries per execution)`);
    }
  };

  /**
   * Execute a query and convert results to objects
   */
  const executeQuery = <T>(sql: string, params?: unknown[]): T[] => {
    validateSQL(sql);

    try {
      const result = deploymentDb.executeRawSQL(sql, params);

      // Convert row arrays to objects
      return result.rows.map(row => {
        const obj: Record<string, unknown> = {};
        result.columns.forEach((col, i) => {
          obj[col] = row[i];
        });
        return obj as T;
      });
    } catch (error) {
      // Re-throw with cleaner message
      const message = error instanceof Error ? error.message : 'Query failed';
      throw new Error(`Database error: ${message}`);
    }
  };

  /**
   * Execute a statement that modifies data
   */
  const executeRun = (sql: string, params?: unknown[]): { changes: number; lastInsertRowid: number | bigint } => {
    validateSQL(sql);

    if (readOnly) {
      throw new Error('Database is in read-only mode');
    }

    try {
      const result = deploymentDb.executeRawSQL(sql, params);
      return {
        changes: result.rowsAffected,
        lastInsertRowid: 0, // SQLite doesn't expose this through our API currently
      };
    } catch (error) {
      const message = error instanceof Error ? error.message : 'Query failed';
      throw new Error(`Database error: ${message}`);
    }
  };

  return {
    query<T = Record<string, unknown>>(sql: string, params?: unknown[]): T[] {
      return executeQuery<T>(sql, params);
    },

    run(sql: string, params?: unknown[]): { changes: number; lastInsertRowid: number | bigint } {
      return executeRun(sql, params);
    },

    all<T = Record<string, unknown>>(sql: string, params?: unknown[]): T[] {
      return executeQuery<T>(sql, params);
    },
  };
}