@rip-lang/db 1.0.1 → 1.0.2

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/db.rip CHANGED
@@ -6,30 +6,38 @@
6
6
  # Fully compatible with the official DuckDB UI!
7
7
  #
8
8
  # Usage:
9
- # rip db.rip <database.duckdb> [--port 4000]
10
- # rip db.rip :memory: --port 4000
9
+ # rip db.rip <database.duckdb> [--port 4213]
10
+ # rip db.rip :memory: --port 4213
11
11
  #
12
- # Then open http://localhost:4000/ for the official DuckDB UI!
12
+ # Then open http://localhost:4213/ for the official DuckDB UI!
13
13
  # (UI assets are proxied from ui.duckdb.org)
14
14
  #
15
- # Endpoints:
16
- # GET / - Official DuckDB UI (proxied from ui.duckdb.org)
17
- # POST /sql - Execute SQL (JSON API)
18
- # POST /ddb/run - DuckDB UI binary protocol
19
- # GET /health - Health check
20
- # GET /status - Database info
21
- # GET /tables - List tables
22
- # GET /schema/:table - Table schema
15
+ # Architecture:
16
+ # - Pure Bun FFI to DuckDB library (no npm package, no Zig)
17
+ # - Binary serialization in Rip for DuckDB UI protocol
18
+ # - Simple, clean, fast
23
19
  #
24
20
  # ==============================================================================
25
21
 
26
22
  import { get, post, read, start, use } from '@rip-lang/api'
27
23
  import { cors } from '@rip-lang/api/middleware'
28
- import { open, queryBinary, tokenize, emptyResult, errorResult } from './lib/duckdb.mjs'
29
-
30
- VERSION = '1.0.0'
31
-
32
- # Enable CORS for duck-ui and other clients
24
+ import { version as VERSION } from './package.json'
25
+
26
+ # Database access via pure Bun FFI
27
+ duckdb = await import('./lib/duckdb.mjs')
28
+ open = duckdb.open
29
+ duckdbVersion = duckdb.version
30
+
31
+ # Binary serialization for DuckDB UI protocol
32
+ {
33
+ serializeSuccessResult
34
+ serializeErrorResult
35
+ serializeEmptyResult
36
+ serializeTokenizeResult
37
+ tokenizeSQL
38
+ } = await import('./lib/duckdb-binary.rip')
39
+
40
+ # Enable CORS for DuckDB UI and other clients
33
41
  use cors preflight: true
34
42
 
35
43
  # Log all requests
@@ -50,7 +58,7 @@ args = process.argv.slice(2)
50
58
  # Handle --help and --version
51
59
  if '--help' in args or '-h' in args
52
60
  console.log """
53
- rip-db v#{VERSION} — DuckDB Server (Bun-native)
61
+ rip-db v#{VERSION} — DuckDB Server (Pure Bun FFI)
54
62
 
55
63
  Usage: rip db.rip [database] [options]
56
64
 
@@ -62,24 +70,17 @@ if '--help' in args or '-h' in args
62
70
  --help Show this help
63
71
  --version Show version
64
72
 
65
- Environment variables (for rip-server):
66
- DB_PATH Path to DuckDB file
67
- DB_PORT Port to listen on
68
-
69
73
  Endpoints:
70
- POST /sql Execute SQL query or statement
71
- GET /health Health check (always ok)
72
- GET /status Database info and table list
73
- GET /tables List all tables
74
- GET /schema/:t Get schema for table t
74
+ GET / Official DuckDB UI
75
+ POST /sql Execute SQL (JSON API)
76
+ POST /ddb/run DuckDB UI binary protocol
77
+ GET /health Health check
78
+ GET /tables List all tables
75
79
 
76
80
  Examples:
77
81
  rip db.rip # In-memory database on port 4213
78
82
  rip db.rip mydb.duckdb # File-based database
79
83
  rip db.rip :memory: --port=8080
80
-
81
- # With rip-server (hot-reloading):
82
- DB_PATH=mydb.duckdb rip-server http db.rip
83
84
  """
84
85
  process.exit(0)
85
86
 
@@ -87,283 +88,277 @@ if '--version' in args or '-v' in args
87
88
  console.log "rip-db v#{VERSION}"
88
89
  process.exit(0)
89
90
 
90
- # Support both env vars (for rip-server) and CLI args (for rip db.rip)
91
+ # Database and port configuration
91
92
  path = process.env.DB_PATH or args.find((a) -> not a.startsWith('-')) or ':memory:'
92
- port = parseInt(process.env.DB_PORT or (args.find((a) -> a.startsWith('--port=')))?.split('=')[1]) or 4213
93
93
 
94
- # Open database and create persistent connection for binary queries
94
+ # Support both --port=N and --port N
95
+ portArg = do ->
96
+ for a, i in args
97
+ return a.split('=')[1] if a.startsWith('--port=')
98
+ return args[i + 1] if a is '--port' and args[i + 1]
99
+ null
100
+ port = parseInt(process.env.DB_PORT or portArg) or 4213
101
+
102
+ # Open database and create persistent connection
95
103
  db = open(path)
96
- binaryConn = db.connect() # Keep one connection for binary protocol queries
97
- console.log "rip-db: database=#{path} (bun-native)"
104
+ conn = db.connect()
105
+ console.log "rip-db: database=#{path} (bun-ffi)"
106
+ console.log "rip-db: DuckDB version=#{duckdbVersion()}"
98
107
 
99
108
  # ==============================================================================
100
109
  # Helpers
101
110
  # ==============================================================================
102
111
 
103
- # Extract column info from result
104
- getColumnInfo = (rows) ->
105
- return { columns: [], types: [] } unless rows?.length
106
- first = rows[0]
107
- columns = Object.keys(first)
108
- types = columns.map (col) ->
109
- switch typeof val = first[col]
110
- when 'number' then Number.isInteger(val) ? 'INTEGER' : 'DOUBLE'
111
- when 'string' then 'VARCHAR'
112
- when 'boolean' then 'BOOLEAN'
113
- when 'bigint' then 'BIGINT'
114
- else val is null ? 'NULL' : 'UNKNOWN'
115
- { columns, types }
116
-
117
- # Convert row objects to arrays for compact format
118
- rowsToArrays = (rows, columns) ->
119
- rows.map (row) -> columns.map (col) -> row[col]
120
-
121
- # Check if SQL is a SELECT-type query
112
+ # Helper for binary responses
113
+ binaryResponse = (data) ->
114
+ new Response data, headers: { 'Content-Type': 'application/octet-stream' }
115
+
116
+ # Check if SQL is a query that returns results
122
117
  isSelectQuery = (sql) ->
123
118
  upper = sql.trim().toUpperCase()
124
119
  upper.startsWith('SELECT') or
125
120
  upper.startsWith('WITH') or
126
121
  upper.startsWith('SHOW') or
127
122
  upper.startsWith('DESCRIBE') or
123
+ upper.startsWith('EXPLAIN') or
128
124
  upper.startsWith('PRAGMA')
129
125
 
130
- # Execute SQL and return JSONCompact result
131
- executeSQL = (sql) ->
126
+ # Log SQL query (truncated if long)
127
+ logSQL = (sql) ->
128
+ preview = sql.replace(/\s+/g, ' ').trim()
129
+ preview = "#{preview.slice(0, 100)}..." if preview.length > 100
130
+ console.log "SQL: #{preview}"
131
+
132
+ # Execute SQL and return result (uses persistent connection)
133
+ executeSQL = (sql, params = []) ->
134
+ logSQL sql
132
135
  startTime = Date.now()
133
- conn = db.connect()
134
136
 
135
137
  try
136
- rows = conn.query(sql)
138
+ rows = await conn.query(sql, params)
139
+ columns = rows.columns or []
137
140
 
138
141
  if isSelectQuery(sql)
139
- { columns, types } = getColumnInfo(rows)
140
- data = rowsToArrays(rows, columns)
141
142
  {
142
- meta: columns.map((name, i) -> { name, type: types[i] })
143
- data: data
144
- rows: data.length
143
+ meta: columns.map((col) -> { name: col.name, type: col.typeName })
144
+ data: rows.map((row) -> columns.map((col) -> row[col.name]))
145
+ rows: rows.length
145
146
  time: (Date.now() - startTime) / 1000
146
147
  }
147
148
  else
148
149
  { meta: [], data: [], rows: 0, time: (Date.now() - startTime) / 1000 }
149
150
  catch err
150
151
  { error: err.message }
151
- finally
152
- conn.close()
153
152
 
154
153
  # ==============================================================================
155
- # Endpoints
154
+ # JSON Endpoints (for custom apps)
156
155
  # ==============================================================================
157
156
 
158
- # POST / — duck-ui compatible (raw SQL in body)
159
- post '/' ->
160
- sql = read 'body', 'string' or return { error: 'Empty query' }
161
- # Log the SQL (truncate long queries)
162
- preview = sql.replace(/\s+/g, ' ').trim()
163
- preview = "#{preview.slice(0, 120)}..." if preview.length > 120
164
- console.log "SQL: #{preview}"
165
- executeSQL sql
166
-
167
- # POST /sql — JSON body with SQL query
157
+ # POST /sqlExecute SQL with optional parameters
168
158
  post '/sql' ->
169
- sql = read 'sql', 'string' or return { error: 'Missing required field: sql' }
170
- preview = sql.replace(/\s+/g, ' ').trim()
171
- preview = "#{preview.slice(0, 120)}..." if preview.length > 120
172
- console.log "SQL: #{preview}"
159
+ contentType = @req.header('content-type') or ''
160
+ if contentType.includes('application/json')
161
+ body = read()
162
+ sql = body?.sql or body
163
+ params = body?.params or []
164
+ else
165
+ sql = read 'body', 'string'
166
+ params = []
167
+ return { error: 'Empty query' } unless sql
168
+ executeSQL sql, params
169
+
170
+ # POST / — Raw SQL in body (duck-ui compatible)
171
+ post '/' ->
172
+ sql = read 'body', 'string'
173
+ return { error: 'Empty query' } unless sql
173
174
  executeSQL sql
174
175
 
175
- # GET /health — Simple health check (no DB query)
176
- get '/health', ->
177
- { ok: true }
178
-
179
- # GET /status — Database info and table list
180
- get '/status', ->
181
- conn = db.connect()
182
- try
183
- tables = conn.query("SELECT table_name FROM information_schema.tables WHERE table_schema = 'main'")
184
- {
185
- ok: true
186
- database: path
187
- tables: tables.map((t) -> t.table_name)
188
- time: new Date().toISOString()
189
- }
190
- catch err
191
- { ok: false, error: err.message }
192
- finally
193
- conn.close()
176
+ # GET /health — Health check
177
+ get '/health' ->
178
+ { status: 'ok', version: VERSION }
194
179
 
195
180
  # GET /tables — List all tables
196
- get '/tables', ->
197
- conn = db.connect()
198
- try
199
- tables = conn.query("SELECT table_name FROM information_schema.tables WHERE table_schema = 'main' ORDER BY table_name")
200
- { ok: true, tables: tables.map((t) -> t.table_name) }
201
- catch err
202
- { ok: false, error: err.message }
203
- finally
204
- conn.close()
181
+ get '/tables' ->
182
+ rows = await conn.query "SELECT table_name FROM information_schema.tables WHERE table_schema = 'main'"
183
+ { tables: rows.map((r) -> r.table_name) }
205
184
 
206
185
  # GET /schema/:table — Get table schema
207
- get '/schema/:table', ->
208
- table = read 'table', 'string!'
209
- conn = db.connect()
186
+ get '/schema/:table' ->
187
+ table = @req.params.table.replace(/[^a-zA-Z0-9_]/g, '') # sanitize
210
188
  try
211
- columns = conn.query("SELECT column_name, data_type, is_nullable FROM information_schema.columns WHERE table_name = '#{table.replace(/'/g, "''")}' ORDER BY ordinal_position")
212
-
213
- if columns.length is 0
214
- { ok: false, error: "Table '#{table}' not found" }
215
- else
216
- { ok: true, table: table, columns: columns }
189
+ rows = await conn.query "DESCRIBE \"#{table}\""
190
+ { schema: rows }
217
191
  catch err
218
- { ok: false, error: err.message }
219
- finally
220
- conn.close()
192
+ { error: err.message }
221
193
 
222
194
  # ==============================================================================
223
- # DuckDB UI Protocol — Binary endpoints for official UI compatibility
224
- # ==============================================================================
225
- #
226
- # These endpoints implement the binary protocol used by DuckDB's official UI.
227
- # This allows you to use the beautiful DuckDB UI with rip-db!
228
- #
229
- # The official UI expects these endpoints:
230
- # POST /ddb/run - Execute SQL (binary response)
231
- # POST /ddb/interrupt - Cancel running query
232
- # POST /ddb/tokenize - Tokenize SQL for syntax highlighting
233
- # GET /info - Server version info
234
- #
235
- # To use the DuckDB UI:
236
- # 1. Start rip-db: rip-db mydb.duckdb --port 4000
237
- # 2. Open: http://localhost:4000 (UI proxied from ui.duckdb.org)
238
- #
195
+ # DuckDB UI Binary Protocol
239
196
  # ==============================================================================
240
197
 
198
+ # Remote UI URL for proxying assets
199
+ UI_REMOTE_URL = 'https://ui.duckdb.org'
200
+
201
+ # Helper to extract parameters from DuckDB UI headers (base64 encoded)
202
+ # The official C++ extension passes all params as strings (VARCHAR).
203
+ # DuckDB handles implicit casting to target types.
204
+ getUIParams = (req) ->
205
+ count = parseInt(req.header('x-duckdb-ui-parameter-count') or '0')
206
+ return [] if count is 0
207
+
208
+ params = []
209
+ for i in [0...count]
210
+ encoded = req.header("x-duckdb-ui-parameter-value-#{i}")
211
+ if encoded
212
+ try
213
+ decoded = atob(encoded)
214
+ # The UI sends literal "null" for NULL values
215
+ if decoded is 'null'
216
+ params.push null
217
+ else
218
+ params.push decoded
219
+ catch
220
+ params.push ''
221
+ else
222
+ params.push null
223
+ params
224
+
241
225
  # POST /ddb/run — Execute SQL and return binary result (DuckDB UI protocol)
242
226
  post '/ddb/run' ->
243
227
  try
244
- sql = read 'body', 'string' or return binaryResponse errorResult 'Empty query'
228
+ sql = read 'body', 'string'
229
+ return binaryResponse serializeErrorResult 'Empty query' unless sql
245
230
 
246
- # Parse row limit from DuckDB UI header
231
+ logSQL sql
247
232
  rowLimit = parseInt(@req.header('x-duckdb-ui-result-row-limit') or '10000')
248
- rowLimit = 10000 if isNaN(rowLimit) or rowLimit <= 0
233
+ params = getUIParams(@req)
249
234
 
250
- # Execute query and get binary result directly from Zig!
251
- # (No JS serialization needed - Zig does it all)
252
- result = queryBinary binaryConn.handle, sql, rowLimit
253
- binaryResponse result
235
+ rows = await conn.query sql, params
236
+ columns = rows.columns or []
237
+
238
+ # Build column info for serialization
239
+ colInfo = columns.map (col) -> { name: col.name, type: col.typeName }
240
+
241
+ # Convert rows to arrays (ensure undefined becomes null for serialization)
242
+ limitedRows = rows.slice(0, rowLimit)
243
+ arrayRows = limitedRows.map (row) ->
244
+ columns.map (col) ->
245
+ val = row[col.name]
246
+ if val is undefined then null else val
247
+
248
+ binaryResponse serializeSuccessResult colInfo, arrayRows
254
249
 
255
250
  catch err
256
- console.error "POST /ddb/run error:", err?.message
257
- binaryResponse errorResult err?.message or 'Unknown error'
251
+ msg = err?.message or 'Unknown error'
252
+ # Handle _duckdb_ui catalog queries - return empty success instead of error
253
+ if msg.includes('_duckdb_ui')
254
+ return binaryResponse serializeSuccessResult [], []
255
+ console.error "POST /ddb/run error:", msg
256
+ binaryResponse serializeErrorResult msg
258
257
 
259
258
  # POST /ddb/interrupt — Cancel running query
260
- post '/ddb/interrupt', ->
261
- # In a real implementation, this would cancel the running query
262
- # For now, just return empty result
263
- binaryResponse emptyResult()
259
+ post '/ddb/interrupt' ->
260
+ binaryResponse serializeEmptyResult()
264
261
 
265
262
  # POST /ddb/tokenize — Tokenize SQL for syntax highlighting
266
263
  post '/ddb/tokenize' ->
267
264
  sql = read 'body', 'string'
268
- binaryResponse tokenize (sql or '')
265
+ tokens = tokenizeSQL(sql or '')
266
+ binaryResponse serializeTokenizeResult(tokens)
267
+
268
+ # ==============================================================================
269
+ # DuckDB UI Compatibility Endpoints
270
+ # (Responses match the official DuckDB UI extension exactly)
271
+ # ==============================================================================
272
+
273
+ # UI extension version — must match what /config advertises for our DuckDB version
274
+ UI_EXT_VERSION = '150-5582dfaffc'
275
+
276
+ # Detect platform for DuckDB headers
277
+ PLATFORM = switch "#{process.platform}_#{process.arch}"
278
+ when 'darwin_arm64' then 'osx_arm64'
279
+ when 'darwin_x64' then 'osx_amd64'
280
+ when 'linux_arm64' then 'linux_arm64'
281
+ when 'linux_x64' then 'linux_amd64'
282
+ else "#{process.platform}_#{process.arch}"
269
283
 
270
284
  # GET /version — Tell UI we're running in host/HTTP mode (not WASM)
271
285
  get '/version' ->
272
- { origin: 'host', version: '139-944c08a214' }
286
+ { origin: 'host', version: "main@e6517d259ec5f27ab713e5755a29d775a7750dc5" }
273
287
 
274
- # GET /localToken — Return empty token for local mode (no MotherDuck)
288
+ # GET /localToken — Return 401 to skip MotherDuck auth (matches official)
275
289
  get '/localToken' ->
276
- new Response '', { status: 200, headers: { 'Content-Type': 'text/plain' } }
290
+ new Response null, { status: 401 }
277
291
 
278
- # GET /info — Server version info (DuckDB UI checks this)
279
- # The UI checks X-DuckDB-UI-Extension-Version to decide HTTP vs WASM mode
280
- # Version format must match desiredDuckDBUIExtensionVersions in /config
292
+ # GET /info — Server version info (headers only, empty body — matches official)
281
293
  get '/info' ->
282
294
  @body '', 200,
283
295
  'Access-Control-Allow-Origin': '*'
284
- 'X-DuckDB-Version': '1.4.1'
285
- 'X-DuckDB-Platform': 'rip-db'
286
- 'X-DuckDB-UI-Extension-Version': '139-944c08a214'
296
+ 'Content-Type': 'text/plain'
297
+ 'X-DuckDB-Version': duckdbVersion()
298
+ 'X-DuckDB-Platform': PLATFORM
299
+ 'X-DuckDB-UI-Extension-Version': UI_EXT_VERSION
287
300
 
288
- # Helper to create binary response
289
- binaryResponse = (buffer) ->
290
- new Response buffer,
291
- headers: { 'Content-Type': 'application/octet-stream' }
292
-
293
- # ==============================================================================
294
- # DuckDB UI Proxy — Serve official UI assets from ui.duckdb.org
295
- # ==============================================================================
296
- #
297
- # The official DuckDB UI is a React app hosted at https://ui.duckdb.org
298
- # We proxy these assets so you can use the beautiful UI with rip-db!
299
- #
300
- # The UI makes API requests to relative URLs (/ddb/run, /ddb/tokenize, etc.)
301
- # which we handle above. This proxy just serves the static UI assets.
302
- #
303
- # ==============================================================================
304
-
305
- UI_REMOTE_URL = 'https://ui.duckdb.org'
306
-
307
- # GET /config — Local mode configuration (prevents MotherDuck redirect)
301
+ # GET /config Proxy from ui.duckdb.org with DuckDB version headers
302
+ # The UI checks these HEADERS (not just /info) to decide HTTP vs WASM mode
308
303
  get '/config' ->
309
- # Return minimal config with required fields for local mode
310
- config =
311
- logging:
312
- enabled: false
313
- datadog:
314
- applicationId: ''
315
- clientToken: ''
316
- site: ''
317
- env: 'local'
318
- allowedTracingOrigins: []
319
- defaultFeatureValues: {}
320
-
321
- # Headers tell UI this is a local DuckDB instance
322
- new Response JSON.stringify(config),
323
- headers:
324
- 'Content-Type': 'application/json'
325
- 'X-DuckDB-Version': '1.4.1'
326
- 'X-DuckDB-Platform': 'osx_arm64'
327
- 'X-DuckDB-UI-Extension-Version': '139-944c08a214'
304
+ try
305
+ response = fetch! "#{UI_REMOTE_URL}/config",
306
+ headers: { 'User-Agent': "rip-db/#{VERSION}" }
307
+ body = response.text!
308
+ new Response body,
309
+ status: response.status
310
+ headers:
311
+ 'Content-Type': 'application/json'
312
+ 'X-DuckDB-Version': duckdbVersion()
313
+ 'X-DuckDB-Platform': PLATFORM
314
+ 'X-DuckDB-UI-Extension-Version': UI_EXT_VERSION
315
+ catch
316
+ new Response JSON.stringify({
317
+ globalApiUrl: "http://localhost:#{port}"
318
+ apiUrl: "http://localhost:#{port}"
319
+ momApiUrl: "http://localhost:#{port}/mom"
320
+ logging: { enabled: false }
321
+ datadog: { applicationId: '', clientToken: '', site: '', env: 'local', allowedTracingOrigins: [] }
322
+ defaultFeatureValues: {}
323
+ desiredDuckDBUIExtensionVersions: { "#{duckdbVersion()}": UI_EXT_VERSION }
324
+ }),
325
+ headers:
326
+ 'Content-Type': 'application/json'
327
+ 'X-DuckDB-Version': duckdbVersion()
328
+ 'X-DuckDB-Platform': PLATFORM
329
+ 'X-DuckDB-UI-Extension-Version': UI_EXT_VERSION
328
330
 
329
- # GET /localEvents — Server-sent events for catalog updates
331
+ # GET /localEvents — SSE stream with keepalive (matches official extension)
332
+ # The official blocks 5s per iteration, sends ":\r\r" keepalive if no events.
330
333
  get '/localEvents' ->
331
- # SSE endpoint for DuckDB UI - must stay open
332
- encoder = new TextEncoder()
333
- stream = new ReadableStream
334
+ keepalive = null
335
+ new Response new ReadableStream({
334
336
  start: (controller) ->
335
- # Send initial connected event
336
- controller.enqueue encoder.encode "event: ConnectedEvent\ndata: \n\n"
337
- # Keep-alive with pings every 5 seconds (Bun has 10s idle timeout)
338
- @interval = setInterval =>
337
+ # Send keepalive every 5 seconds (SSE comment, matches official ":\r\r")
338
+ keepalive = setInterval ->
339
339
  try
340
- controller.enqueue encoder.encode ": ping\n\n"
340
+ controller.enqueue ":\r\r"
341
341
  catch
342
- clearInterval @interval
342
+ clearInterval keepalive
343
343
  , 5000
344
344
  cancel: ->
345
- clearInterval @interval if @interval
346
-
347
- new Response stream,
345
+ clearInterval keepalive if keepalive
346
+ }),
348
347
  headers:
349
348
  'Content-Type': 'text/event-stream'
350
- 'Cache-Control': 'no-cache'
351
- 'Connection': 'keep-alive'
349
+
350
+ # GET /mom/* — MotherDuck API stubs (prevent OAuth, return 401)
351
+ get '/mom/*' ->
352
+ new Response null, { status: 401 }
352
353
 
353
354
  # GET /* — Proxy UI assets from ui.duckdb.org (catch-all, must be last)
354
355
  get '/*' ->
355
- path = @req.path
356
-
357
- # Skip if this is one of our API endpoints (shouldn't reach here, but safety)
358
- return { error: 'Not found' } if path in ['/health', '/status', '/tables', '/info']
356
+ reqPath = @req.path
359
357
 
360
358
  try
361
- # Fetch from remote UI server
362
- response = fetch! "#{UI_REMOTE_URL}#{path}",
363
- headers:
364
- 'User-Agent': "rip-db/#{VERSION}"
359
+ response = fetch! "#{UI_REMOTE_URL}#{reqPath}",
360
+ headers: { 'User-Agent': "rip-db/#{VERSION}" }
365
361
 
366
- # Get response body and content type
367
362
  body = response.arrayBuffer!
368
363
  contentType = response.headers.get('Content-Type') or 'application/octet-stream'
369
364
 
@@ -371,18 +366,17 @@ get '/*' ->
371
366
  headers =
372
367
  'Content-Type': contentType
373
368
  'Cache-Control': response.headers.get('Cache-Control') or 'public, max-age=3600'
369
+ # COOP/COEP required for SharedArrayBuffer (DuckDB WASM fallback)
370
+ 'Cross-Origin-Opener-Policy': 'same-origin'
371
+ 'Cross-Origin-Embedder-Policy': 'credentialless'
374
372
 
375
- # For /config endpoint, add DuckDB version headers (UI uses these to detect mode)
376
- if path is '/config'
377
- headers['X-DuckDB-Version'] = '1.4.1'
378
- headers['X-DuckDB-Platform'] = 'rip-db'
379
- headers['X-DuckDB-UI-Extension-Version'] = '139-944c08a214'
380
-
381
- new Response body, { status: response.status, headers }
373
+ new Response body,
374
+ status: response.status
375
+ headers: headers
382
376
 
383
377
  catch err
384
- console.error "Proxy error for #{path}:", err?.message
385
- new Response "Failed to fetch UI asset: #{path}", { status: 502 }
378
+ console.error "Proxy error for #{reqPath}:", err?.message
379
+ new Response "Failed to fetch UI asset: #{reqPath}", { status: 502 }
386
380
 
387
381
  # ==============================================================================
388
382
  # Start Server