spindb 0.31.4 → 0.33.1

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.
Files changed (64) hide show
  1. package/LICENSE +8 -0
  2. package/README.md +107 -826
  3. package/cli/commands/create.ts +5 -1
  4. package/cli/commands/engines.ts +256 -1
  5. package/cli/commands/menu/backup-handlers.ts +16 -0
  6. package/cli/commands/menu/container-handlers.ts +170 -17
  7. package/cli/commands/menu/engine-handlers.ts +6 -0
  8. package/cli/commands/menu/settings-handlers.ts +6 -0
  9. package/cli/commands/menu/shell-handlers.ts +74 -14
  10. package/cli/commands/menu/sql-handlers.ts +8 -50
  11. package/cli/commands/menu/validators.ts +8 -0
  12. package/cli/commands/users.ts +264 -0
  13. package/cli/constants.ts +8 -0
  14. package/cli/helpers.ts +140 -0
  15. package/cli/index.ts +2 -0
  16. package/cli/ui/prompts.ts +24 -20
  17. package/config/backup-formats.ts +28 -0
  18. package/config/engine-defaults.ts +26 -0
  19. package/config/engines-registry.ts +1 -0
  20. package/config/engines.json +50 -0
  21. package/config/engines.schema.json +6 -1
  22. package/core/base-binary-manager.ts +6 -1
  23. package/core/config-manager.ts +20 -0
  24. package/core/credential-manager.ts +257 -0
  25. package/core/dependency-manager.ts +5 -0
  26. package/core/docker-exporter.ts +30 -0
  27. package/core/error-handler.ts +19 -0
  28. package/engines/base-engine.ts +32 -1
  29. package/engines/clickhouse/index.ts +99 -3
  30. package/engines/cockroachdb/index.ts +69 -2
  31. package/engines/couchdb/index.ts +149 -1
  32. package/engines/ferretdb/README.md +4 -0
  33. package/engines/ferretdb/index.ts +342 -13
  34. package/engines/index.ts +8 -0
  35. package/engines/influxdb/README.md +180 -0
  36. package/engines/influxdb/api-client.ts +64 -0
  37. package/engines/influxdb/backup.ts +160 -0
  38. package/engines/influxdb/binary-manager.ts +110 -0
  39. package/engines/influxdb/binary-urls.ts +69 -0
  40. package/engines/influxdb/hostdb-releases.ts +23 -0
  41. package/engines/influxdb/index.ts +1227 -0
  42. package/engines/influxdb/restore.ts +417 -0
  43. package/engines/influxdb/version-maps.ts +75 -0
  44. package/engines/influxdb/version-validator.ts +128 -0
  45. package/engines/mariadb/index.ts +96 -1
  46. package/engines/meilisearch/index.ts +97 -1
  47. package/engines/mongodb/index.ts +82 -0
  48. package/engines/mysql/index.ts +105 -1
  49. package/engines/postgresql/index.ts +92 -0
  50. package/engines/qdrant/index.ts +107 -2
  51. package/engines/redis/index.ts +106 -12
  52. package/engines/surrealdb/index.ts +102 -2
  53. package/engines/typedb/backup.ts +167 -0
  54. package/engines/typedb/binary-manager.ts +200 -0
  55. package/engines/typedb/binary-urls.ts +38 -0
  56. package/engines/typedb/cli-utils.ts +210 -0
  57. package/engines/typedb/hostdb-releases.ts +118 -0
  58. package/engines/typedb/index.ts +1275 -0
  59. package/engines/typedb/restore.ts +377 -0
  60. package/engines/typedb/version-maps.ts +48 -0
  61. package/engines/typedb/version-validator.ts +127 -0
  62. package/engines/valkey/index.ts +70 -2
  63. package/package.json +4 -1
  64. package/types/index.ts +37 -0
@@ -7,7 +7,11 @@ import { paths } from '../../config/paths'
7
7
  import { getEngineDefaults } from '../../config/defaults'
8
8
  import { platformService, isWindows } from '../../core/platform-service'
9
9
  import { configManager } from '../../core/config-manager'
10
- import { logDebug, logWarning } from '../../core/error-handler'
10
+ import {
11
+ logDebug,
12
+ logWarning,
13
+ assertValidUsername,
14
+ } from '../../core/error-handler'
11
15
  import { processManager } from '../../core/process-manager'
12
16
  import { portManager } from '../../core/port-manager'
13
17
  import { couchdbBinaryManager } from './binary-manager'
@@ -37,6 +41,8 @@ import {
37
41
  type StatusResult,
38
42
  type QueryResult,
39
43
  type QueryOptions,
44
+ type CreateUserOptions,
45
+ type UserCredentials,
40
46
  } from '../../types'
41
47
  import { parseRESTAPIResult } from '../../core/query-parser'
42
48
 
@@ -1276,6 +1282,148 @@ export class CouchDBEngine extends BaseEngine {
1276
1282
 
1277
1283
  return databases
1278
1284
  }
1285
+
1286
+ async createUser(
1287
+ container: ContainerConfig,
1288
+ options: CreateUserOptions,
1289
+ ): Promise<UserCredentials> {
1290
+ const { username, password, database } = options
1291
+ assertValidUsername(username)
1292
+ const { port } = container
1293
+ const db = database || container.database
1294
+ if (!db) {
1295
+ throw new Error(
1296
+ 'No database specified. Use --database or create a database first.',
1297
+ )
1298
+ }
1299
+
1300
+ // Ensure _users system database exists (CouchDB 3.x doesn't auto-create it)
1301
+ const usersDbResponse = await couchdbApiRequest(port, 'PUT', '/_users')
1302
+ if (usersDbResponse.status !== 201 && usersDbResponse.status !== 412) {
1303
+ throw new Error(
1304
+ `Failed to ensure _users database exists: ${JSON.stringify(usersDbResponse.data)}`,
1305
+ )
1306
+ }
1307
+
1308
+ // Create user document in _users database
1309
+ const userDoc = {
1310
+ _id: `org.couchdb.user:${username}`,
1311
+ name: username,
1312
+ type: 'user',
1313
+ roles: [],
1314
+ password,
1315
+ }
1316
+
1317
+ const createResponse = await couchdbApiRequest(
1318
+ port,
1319
+ 'PUT',
1320
+ `/_users/org.couchdb.user:${encodeURIComponent(username)}`,
1321
+ userDoc as unknown as Record<string, unknown>,
1322
+ )
1323
+
1324
+ if (createResponse.status !== 201 && createResponse.status !== 409) {
1325
+ throw new Error(
1326
+ `Failed to create user: ${JSON.stringify(createResponse.data)}`,
1327
+ )
1328
+ }
1329
+
1330
+ if (createResponse.status === 409) {
1331
+ // User exists — fetch current doc revision and update password
1332
+ const getResponse = await couchdbApiRequest(
1333
+ port,
1334
+ 'GET',
1335
+ `/_users/org.couchdb.user:${encodeURIComponent(username)}`,
1336
+ )
1337
+
1338
+ if (getResponse.status !== 200 || !getResponse.data) {
1339
+ throw new Error(
1340
+ `Failed to fetch existing user "${username}" (status ${getResponse.status}): ${JSON.stringify(getResponse.data)}`,
1341
+ )
1342
+ }
1343
+
1344
+ const existingDoc = getResponse.data as Record<string, unknown>
1345
+ const rev = existingDoc._rev as string
1346
+ if (!rev) {
1347
+ throw new Error(
1348
+ `User "${username}" already exists but document has no _rev field: ${JSON.stringify(getResponse.data)}`,
1349
+ )
1350
+ }
1351
+ const updateResponse = await couchdbApiRequest(
1352
+ port,
1353
+ 'PUT',
1354
+ `/_users/org.couchdb.user:${encodeURIComponent(username)}`,
1355
+ { ...existingDoc, password, _rev: rev },
1356
+ )
1357
+ if (updateResponse.status !== 201) {
1358
+ throw new Error(
1359
+ `Failed to update user: ${JSON.stringify(updateResponse.data)}`,
1360
+ )
1361
+ }
1362
+ }
1363
+
1364
+ // Ensure the target database exists before setting security
1365
+ const dbCreateResponse = await couchdbApiRequest(
1366
+ port,
1367
+ 'PUT',
1368
+ `/${encodeURIComponent(db)}`,
1369
+ )
1370
+ // 201 = created, 412 = already exists — both are fine
1371
+ if (dbCreateResponse.status !== 201 && dbCreateResponse.status !== 412) {
1372
+ throw new Error(
1373
+ `Failed to ensure database "${db}" exists: ${JSON.stringify(dbCreateResponse.data)}`,
1374
+ )
1375
+ }
1376
+
1377
+ // Grant access to the target database via _security document
1378
+ const secResponse = await couchdbApiRequest(
1379
+ port,
1380
+ 'GET',
1381
+ `/${encodeURIComponent(db)}/_security`,
1382
+ )
1383
+
1384
+ if (secResponse.status !== 200) {
1385
+ throw new Error(
1386
+ `Failed to read database security for "${db}": ${JSON.stringify(secResponse.data)}`,
1387
+ )
1388
+ }
1389
+
1390
+ const security = (secResponse.data || {}) as Record<string, unknown>
1391
+ const members = (security.members || {}) as Record<string, unknown>
1392
+ const names = ((members.names || []) as string[]).slice()
1393
+
1394
+ if (!names.includes(username)) {
1395
+ names.push(username)
1396
+ }
1397
+
1398
+ const secPutResponse = await couchdbApiRequest(
1399
+ port,
1400
+ 'PUT',
1401
+ `/${encodeURIComponent(db)}/_security`,
1402
+ {
1403
+ ...security,
1404
+ members: { ...members, names },
1405
+ },
1406
+ )
1407
+
1408
+ if (secPutResponse.status !== 200 && secPutResponse.status !== 201) {
1409
+ throw new Error(
1410
+ `Failed to update database security for "${db}": ${JSON.stringify(secPutResponse.data)}`,
1411
+ )
1412
+ }
1413
+
1414
+ logDebug(`Created CouchDB user: ${username}`)
1415
+
1416
+ const connectionString = `http://${encodeURIComponent(username)}:${encodeURIComponent(password)}@127.0.0.1:${port}/${encodeURIComponent(db)}`
1417
+
1418
+ return {
1419
+ username,
1420
+ password,
1421
+ connectionString,
1422
+ engine: container.engine,
1423
+ container: container.name,
1424
+ database: db,
1425
+ }
1426
+ }
1279
1427
  }
1280
1428
 
1281
1429
  export const couchdbEngine = new CouchDBEngine()
@@ -23,6 +23,10 @@ This is a **composite engine** with unique binary management requirements.
23
23
 
24
24
  FerretDB is **not available on Windows** due to postgresql-documentdb startup issues. The Windows binaries exist in hostdb, but the PostgreSQL backend fails to initialize properly. This has been extensively tested and currently requires WSL as a workaround.
25
25
 
26
+ ### macOS SIP / Container Limitations
27
+
28
+ On macOS, System Integrity Protection (SIP) can block creating symlinks in system directories (e.g., `/usr/local`). In containerized or locked-down environments, even `sudo` may not permit writes to those paths. If you hit permission errors during setup, use a non-system install location or run with elevated privileges when available. See https://github.com/robertjbass/spindb#ferretdb for details.
29
+
26
30
  ## Binary Packaging
27
31
 
28
32
  ### Archive Format
@@ -15,8 +15,15 @@ import { spawn, exec, type SpawnOptions } from 'child_process'
15
15
  import { promisify } from 'util'
16
16
  import { existsSync } from 'fs'
17
17
  import net from 'net'
18
- import { mkdir, writeFile, readFile, unlink } from 'fs/promises'
19
- import { join, basename } from 'path'
18
+ import {
19
+ mkdir,
20
+ writeFile,
21
+ readFile,
22
+ symlink,
23
+ unlink,
24
+ readdir,
25
+ } from 'fs/promises'
26
+ import { join, basename, dirname } from 'path'
20
27
  import { BaseEngine } from '../base-engine'
21
28
  import { paths } from '../../config/paths'
22
29
  import { getEngineDefaults } from '../../config/defaults'
@@ -27,6 +34,7 @@ import {
27
34
  logDebug,
28
35
  logWarning,
29
36
  assertValidDatabaseName,
37
+ assertValidUsername,
30
38
  } from '../../core/error-handler'
31
39
  import { processManager } from '../../core/process-manager'
32
40
  import { spawnAsync } from '../../core/spawn-utils'
@@ -57,6 +65,8 @@ import {
57
65
  type StatusResult,
58
66
  type QueryResult,
59
67
  type QueryOptions,
68
+ type CreateUserOptions,
69
+ type UserCredentials,
60
70
  } from '../../types'
61
71
  import { parseMongoDBResult } from '../../core/query-parser'
62
72
 
@@ -332,12 +342,130 @@ export class FerretDBEngine extends BaseEngine {
332
342
  arch,
333
343
  )
334
344
 
345
+ // Homebrew-derived x64 binaries have compiled-in absolute paths for
346
+ // sharedir, pkglibdir ($libdir), and libdir that don't exist when running
347
+ // from ~/.spindb/bin/. We fix this by:
348
+ // 1. Using initdb's -L flag to explicitly set the share directory
349
+ // 2. Creating symlinks at compiled-in paths for pkglibdir and libdir
350
+ // (these are needed by the bootstrap postgres subprocess during initdb)
351
+ const shareDirBase = join(documentdbPath, 'share')
352
+ const actualShareDir = existsSync(join(shareDirBase, 'postgres.bki'))
353
+ ? shareDirBase
354
+ : existsSync(join(shareDirBase, 'postgresql', 'postgres.bki'))
355
+ ? join(shareDirBase, 'postgresql')
356
+ : shareDirBase
357
+
358
+ // Homebrew-derived binaries have compiled-in absolute paths that only
359
+ // need fixup on macOS. On Linux the paths are relative or handled by
360
+ // LD_LIBRARY_PATH, so skip the pg_config symlink fixups entirely.
361
+ if (platform === 'darwin') {
362
+ const pgConfigBin = join(documentdbPath, 'bin', `pg_config${ext}`)
363
+ if (existsSync(pgConfigBin)) {
364
+ // Query all relevant compiled-in paths and create symlinks where needed
365
+ const pathFixups: Array<{
366
+ flag: string
367
+ actualDir: string
368
+ label: string
369
+ }> = [
370
+ { flag: '--sharedir', actualDir: actualShareDir, label: 'share' },
371
+ {
372
+ flag: '--pkglibdir',
373
+ actualDir: existsSync(join(documentdbPath, 'lib', 'postgresql'))
374
+ ? join(documentdbPath, 'lib', 'postgresql')
375
+ : join(documentdbPath, 'lib'),
376
+ label: 'pkglib',
377
+ },
378
+ {
379
+ flag: '--libdir',
380
+ actualDir: join(documentdbPath, 'lib'),
381
+ label: 'lib',
382
+ },
383
+ ]
384
+
385
+ // Create symlinks at compiled-in paths so PostgreSQL can find its
386
+ // libraries. These paths may be in system directories (e.g. /usr/local/),
387
+ // which require elevated privileges to write to.
388
+ for (const { flag, actualDir, label } of pathFixups) {
389
+ try {
390
+ const { stdout: out } = await execAsync(
391
+ `"${pgConfigBin}" ${flag}`,
392
+ { timeout: 5000 },
393
+ )
394
+ const compiledDir = out.trim()
395
+ logDebug(`pg_config ${flag}: ${compiledDir}`)
396
+ if (compiledDir && !existsSync(compiledDir)) {
397
+ await mkdir(dirname(compiledDir), { recursive: true })
398
+ await symlink(actualDir, compiledDir)
399
+ logDebug(
400
+ `Created ${label} symlink: ${compiledDir} -> ${actualDir}`,
401
+ )
402
+ }
403
+ } catch (error) {
404
+ const e = error as NodeJS.ErrnoException
405
+ const isPermission =
406
+ e.code === 'EACCES' ||
407
+ e.code === 'EPERM' ||
408
+ (e.message && /permission denied/i.test(e.message))
409
+ if (isPermission) {
410
+ logWarning(
411
+ `Cannot create ${label} symlink (permission denied). ` +
412
+ `This can be caused by macOS SIP or container/sudo limitations when compiled-in paths point to system directories. ` +
413
+ `Workaround: use a non-system install path, or run with elevated privileges if available (e.g., sudo spindb engines download ferretdb <version>). ` +
414
+ `See https://github.com/robertjbass/spindb#ferretdb for details. ` +
415
+ `Target: ${flag} -> ${actualDir}`,
416
+ )
417
+ } else {
418
+ logDebug(`Could not fix compiled ${label} path: ${e.message}`)
419
+ }
420
+ }
421
+ }
422
+ }
423
+ } else {
424
+ logDebug(
425
+ 'Skipping pg_config symlink fixups (not required on this platform)',
426
+ )
427
+ }
428
+
429
+ // On macOS, fix hardcoded Homebrew dylib paths in extension libraries.
430
+ // The x64 build may have extensions (e.g. pg_documentdb_core.dylib) that
431
+ // reference Homebrew libraries (e.g. libbson2.2.dylib from mongo-c-driver)
432
+ // via absolute paths that don't exist on the target machine.
433
+ if (platform === 'darwin') {
434
+ const dylibMarker = join(documentdbPath, '.dylib_fix_done')
435
+ if (!existsSync(dylibMarker)) {
436
+ await this.fixDylibDependencies(documentdbPath)
437
+ try {
438
+ await writeFile(dylibMarker, '', { flag: 'wx' })
439
+ } catch {
440
+ // Marker may already exist from a parallel init — safe to ignore
441
+ }
442
+ }
443
+ }
444
+
445
+ // On macOS, set DYLD_FALLBACK_LIBRARY_PATH as additional library search path.
446
+ // Unlike DYLD_LIBRARY_PATH, this is NOT stripped by SIP.
447
+ const initdbEnv =
448
+ platform === 'darwin'
449
+ ? {
450
+ ...spawnEnv,
451
+ DYLD_FALLBACK_LIBRARY_PATH: join(documentdbPath, 'lib'),
452
+ }
453
+ : spawnEnv
454
+
335
455
  try {
336
- // Add timeout to prevent hanging on Windows
337
456
  await spawnAsync(
338
457
  initdb,
339
- ['-D', pgDataDir, '-U', 'postgres', '--encoding=UTF8', '--locale=C'],
340
- { env: spawnEnv, timeout: 60000 },
458
+ [
459
+ '-D',
460
+ pgDataDir,
461
+ '-U',
462
+ 'postgres',
463
+ '--encoding=UTF8',
464
+ '--locale=C',
465
+ '-L',
466
+ actualShareDir,
467
+ ],
468
+ { env: initdbEnv, timeout: 60000 },
341
469
  )
342
470
  logDebug(`Initialized PostgreSQL data directory: ${pgDataDir}`)
343
471
  } catch (error) {
@@ -347,11 +475,11 @@ export class FerretDBEngine extends BaseEngine {
347
475
 
348
476
  // Copy the bundled postgresql.conf.sample to ensure shared_preload_libraries is set
349
477
  // This is critical for DocumentDB extension to load properly
350
- const bundledConf = join(
351
- documentdbPath,
352
- 'share',
353
- 'postgresql.conf.sample',
478
+ const bundledConf = existsSync(
479
+ join(shareDirBase, 'postgresql.conf.sample'),
354
480
  )
481
+ ? join(shareDirBase, 'postgresql.conf.sample')
482
+ : join(shareDirBase, 'postgresql', 'postgresql.conf.sample')
355
483
  const pgConf = join(pgDataDir, 'postgresql.conf')
356
484
 
357
485
  if (existsSync(bundledConf)) {
@@ -433,12 +561,21 @@ export class FerretDBEngine extends BaseEngine {
433
561
  arch,
434
562
  )
435
563
 
436
- // Get spawn env for Linux (LD_LIBRARY_PATH for postgresql-documentdb binaries)
437
- const pgSpawnEnv = ferretdbBinaryManager.getDocumentDBSpawnEnv(
564
+ // Get spawn env for postgresql-documentdb binaries:
565
+ // - Linux: LD_LIBRARY_PATH for shared libraries
566
+ // - macOS: DYLD_FALLBACK_LIBRARY_PATH (not stripped by SIP)
567
+ const baseSpawnEnv = ferretdbBinaryManager.getDocumentDBSpawnEnv(
438
568
  fullBackendVersion,
439
569
  platform,
440
570
  arch,
441
571
  )
572
+ const pgSpawnEnv =
573
+ platform === 'darwin'
574
+ ? {
575
+ ...baseSpawnEnv,
576
+ DYLD_FALLBACK_LIBRARY_PATH: join(documentdbPath, 'lib'),
577
+ }
578
+ : baseSpawnEnv
442
579
 
443
580
  const ext = platformService.getExecutableExtension()
444
581
  const ferretdbBinary = join(ferretdbPath, 'bin', `ferretdb${ext}`)
@@ -467,6 +604,20 @@ export class FerretDBEngine extends BaseEngine {
467
604
  // Allocate backend port
468
605
  const backendPort = existingBackendPort || (await allocateBackendPort())
469
606
 
607
+ // Fix hardcoded Homebrew dylib paths (darwin-x64 binaries)
608
+ // Skip if already completed (marker written by initDataDir or a previous start)
609
+ if (platform === 'darwin') {
610
+ const dylibMarker = join(documentdbPath, '.dylib_fix_done')
611
+ if (!existsSync(dylibMarker)) {
612
+ await this.fixDylibDependencies(documentdbPath)
613
+ try {
614
+ await writeFile(dylibMarker, '', { flag: 'wx' })
615
+ } catch {
616
+ // Marker may already exist — safe to ignore
617
+ }
618
+ }
619
+ }
620
+
470
621
  let pgStarted = false
471
622
  let ferretStarted = false
472
623
 
@@ -699,12 +850,19 @@ export class FerretDBEngine extends BaseEngine {
699
850
  arch,
700
851
  )
701
852
 
702
- // Get spawn env for Linux (LD_LIBRARY_PATH for postgresql-documentdb binaries)
703
- const pgSpawnEnv = ferretdbBinaryManager.getDocumentDBSpawnEnv(
853
+ // Get spawn env for postgresql-documentdb binaries
854
+ const baseStopEnv = ferretdbBinaryManager.getDocumentDBSpawnEnv(
704
855
  fullBackendVersion,
705
856
  platform,
706
857
  arch,
707
858
  )
859
+ const pgSpawnEnv =
860
+ platform === 'darwin'
861
+ ? {
862
+ ...baseStopEnv,
863
+ DYLD_FALLBACK_LIBRARY_PATH: join(documentdbPath, 'lib'),
864
+ }
865
+ : baseStopEnv
708
866
 
709
867
  const ext = platformService.getExecutableExtension()
710
868
  const pgCtl = join(documentdbPath, 'bin', `pg_ctl${ext}`)
@@ -725,6 +883,99 @@ export class FerretDBEngine extends BaseEngine {
725
883
  logDebug('FerretDB stopped')
726
884
  }
727
885
 
886
+ /**
887
+ * Fix hardcoded Homebrew dylib paths in extension libraries.
888
+ *
889
+ * The x64 darwin build of postgresql-documentdb has extensions whose dylib
890
+ * load commands reference absolute Homebrew paths (e.g.
891
+ * /usr/local/opt/mongo-c-driver/lib/libbson2.2.dylib). When these paths
892
+ * don't exist on the target machine, the extension fails to load.
893
+ *
894
+ * This method scans extension dylibs with `otool -L`, finds missing
895
+ * dependencies, searches our bundle for matching libraries, and creates
896
+ * symlinks at the expected Homebrew paths.
897
+ */
898
+ private async fixDylibDependencies(documentdbPath: string): Promise<void> {
899
+ const libDir = join(documentdbPath, 'lib')
900
+ const pkgLibDir = join(libDir, 'postgresql')
901
+
902
+ if (!existsSync(pkgLibDir)) return
903
+
904
+ // Collect all .dylib files in our bundle's lib/ directory
905
+ const bundledLibNames = new Set<string>()
906
+ const bundledLibPaths = new Map<string, string>()
907
+ try {
908
+ const libFiles = await readdir(libDir)
909
+ for (const f of libFiles) {
910
+ if (f.endsWith('.dylib')) {
911
+ bundledLibNames.add(f)
912
+ bundledLibPaths.set(f, join(libDir, f))
913
+ }
914
+ }
915
+ } catch {
916
+ return
917
+ }
918
+
919
+ // Scan extension dylibs for missing dependencies
920
+ let extFiles: string[]
921
+ try {
922
+ extFiles = (await readdir(pkgLibDir)).filter((f) => f.endsWith('.dylib'))
923
+ } catch {
924
+ return
925
+ }
926
+
927
+ for (const extFile of extFiles) {
928
+ const extPath = join(pkgLibDir, extFile)
929
+ try {
930
+ const { stdout } = await execAsync(`otool -L "${extPath}"`, {
931
+ timeout: 5000,
932
+ })
933
+
934
+ for (const line of stdout.split('\n')) {
935
+ const match = line.trim().match(/^(\/[^\s]+\.dylib)\s/)
936
+ if (!match) continue
937
+ const depPath = match[1]
938
+
939
+ // Skip system libs, @-prefixed paths, and paths in our bundle
940
+ if (depPath.startsWith('/usr/lib/')) continue
941
+ if (depPath.startsWith('/System/')) continue
942
+ if (depPath.startsWith('@')) continue
943
+ if (depPath.includes(documentdbPath)) continue
944
+
945
+ if (!existsSync(depPath)) {
946
+ const depName = basename(depPath)
947
+
948
+ // Check if we have this exact library in our bundle
949
+ if (bundledLibPaths.has(depName)) {
950
+ try {
951
+ await mkdir(dirname(depPath), { recursive: true })
952
+ } catch {
953
+ logDebug(
954
+ `Cannot create directory for dylib dep: ${dirname(depPath)} (skipping)`,
955
+ )
956
+ continue
957
+ }
958
+ try {
959
+ await symlink(bundledLibPaths.get(depName)!, depPath)
960
+ logDebug(
961
+ `Fixed dylib dep: ${depPath} -> ${bundledLibPaths.get(depName)}`,
962
+ )
963
+ } catch {
964
+ // Symlink may already exist from a parallel fix
965
+ }
966
+ } else {
967
+ logDebug(
968
+ `Missing dylib dependency: ${depPath} (not found in bundle)`,
969
+ )
970
+ }
971
+ }
972
+ }
973
+ } catch {
974
+ logDebug(`Could not scan dylib deps for ${extFile}`)
975
+ }
976
+ }
977
+ }
978
+
728
979
  /**
729
980
  * Stop FerretDB proxy process
730
981
  */
@@ -1343,6 +1594,84 @@ export class FerretDBEngine extends BaseEngine {
1343
1594
  })
1344
1595
  })
1345
1596
  }
1597
+
1598
+ async createUser(
1599
+ container: ContainerConfig,
1600
+ options: CreateUserOptions,
1601
+ ): Promise<UserCredentials> {
1602
+ const { username, password, database } = options
1603
+ assertValidUsername(username)
1604
+ const { port } = container
1605
+ const db = database ?? container.database ?? 'admin'
1606
+ assertValidDatabaseName(db)
1607
+ const mongosh = await this.getMongoshPath()
1608
+
1609
+ // Same as MongoDB - auth disabled with --no-auth but user is still created
1610
+ // Use JSON.stringify for password to safely escape all special characters in JS context
1611
+ // Pass script via stdin to avoid exposing passwords in process listings
1612
+ const jsonPwd = JSON.stringify(password)
1613
+ const script = `db.getSiblingDB('${db}').createUser({user:'${username}',pwd:${jsonPwd},roles:[{role:'readWrite',db:'${db}'}]})`
1614
+
1615
+ const mongoshArgs = ['--host', '127.0.0.1', '--port', String(port), 'admin']
1616
+
1617
+ const runMongoshViaStdin = (js: string): Promise<void> =>
1618
+ new Promise((resolve, reject) => {
1619
+ const proc = spawn(mongosh, mongoshArgs, {
1620
+ stdio: ['pipe', 'pipe', 'pipe'],
1621
+ })
1622
+
1623
+ let stderr = ''
1624
+ proc.stderr?.on('data', (data: Buffer) => {
1625
+ stderr += data.toString()
1626
+ })
1627
+
1628
+ const timeout = setTimeout(() => {
1629
+ proc.kill('SIGTERM')
1630
+ reject(new Error('mongosh timed out after 10 seconds'))
1631
+ }, 10000)
1632
+
1633
+ proc.on('error', (err) => {
1634
+ clearTimeout(timeout)
1635
+ reject(err)
1636
+ })
1637
+
1638
+ proc.on('close', (code) => {
1639
+ clearTimeout(timeout)
1640
+ if (code === 0) resolve()
1641
+ else reject(new Error(stderr || `mongosh exited with code ${code}`))
1642
+ })
1643
+
1644
+ proc.stdin?.write(js)
1645
+ proc.stdin?.end()
1646
+ })
1647
+
1648
+ try {
1649
+ await runMongoshViaStdin(script)
1650
+ } catch (error) {
1651
+ const err = error as Error
1652
+ if (
1653
+ err.message.includes('51003') ||
1654
+ err.message.includes('already exists')
1655
+ ) {
1656
+ // User exists — update password instead
1657
+ const updateScript = `db.getSiblingDB('${db}').updateUser('${username}',{pwd:${jsonPwd}})`
1658
+ await runMongoshViaStdin(updateScript)
1659
+ } else {
1660
+ throw error
1661
+ }
1662
+ }
1663
+
1664
+ const connectionString = `mongodb://${encodeURIComponent(username)}:${encodeURIComponent(password)}@127.0.0.1:${port}/${db}`
1665
+
1666
+ return {
1667
+ username,
1668
+ password,
1669
+ connectionString,
1670
+ engine: container.engine,
1671
+ container: container.name,
1672
+ database: db,
1673
+ }
1674
+ }
1346
1675
  }
1347
1676
 
1348
1677
  export const ferretdbEngine = new FerretDBEngine()
package/engines/index.ts CHANGED
@@ -14,6 +14,8 @@ import { couchdbEngine } from './couchdb'
14
14
  import { cockroachdbEngine } from './cockroachdb'
15
15
  import { surrealdbEngine } from './surrealdb'
16
16
  import { questdbEngine } from './questdb'
17
+ import { typedbEngine } from './typedb'
18
+ import { influxdbEngine } from './influxdb'
17
19
  import { platformService } from '../core/platform-service'
18
20
  import { Engine, Platform } from '../types'
19
21
  import type { BaseEngine } from './base-engine'
@@ -76,6 +78,12 @@ export const engines: Record<string, BaseEngine> = {
76
78
  // QuestDB and aliases
77
79
  [Engine.QuestDB]: questdbEngine,
78
80
  quest: questdbEngine,
81
+ // TypeDB and aliases
82
+ [Engine.TypeDB]: typedbEngine,
83
+ tdb: typedbEngine,
84
+ // InfluxDB and aliases
85
+ [Engine.InfluxDB]: influxdbEngine,
86
+ influx: influxdbEngine,
79
87
  }
80
88
 
81
89
  // Get an engine by name