Skip to content

Conversation

@tvarohohlavy
Copy link
Contributor

@tvarohohlavy tvarohohlavy commented Dec 1, 2025

feat: Add JSON configuration import for Infrastructure as Code support

Summary

Adds the ability to configure Zerobyte entirely via a JSON configuration file, enabling Infrastructure as Code (IaC) workflows, Docker Compose deployments, and migration between instances.

Features

Configuration File Support

  • Load configuration from a JSON file or stdin
  • Environment variable interpolation with ${VAR_NAME} syntax for secure secret injection
  • ArkType schema validation with detailed error messages and paths
  • Create-only semantics: existing resources are not overwritten

CLI Command

bun run cli import-config --config /path/to/config.json
bun run cli import-config --stdin
bun run cli import-config --config /path/to/config.json --dry-run

Options:

Option Description
--config <path> Path to the configuration file
--stdin Read configuration from stdin
--dry-run Validate config without importing
--json Output results in JSON format
--log-level <level> Set log level (debug, info, warn, error)
--overwrite-recovery-key Overwrite existing recovery key (only if database is empty)

Supported Resources

  • Volumes: Local directories, NFS, SMB, WebDAV, SFTP backends
  • Repositories: Local (new or existing), S3, R2, GCS, Azure, SFTP, WebDAV, rclone backends
  • Notification Destinations: Email, Slack, Discord, Gotify, ntfy, Pushover, Telegram, Custom (shoutrrr)
  • Backup Schedules: Cron expressions, retention policies, include/exclude patterns, mirrors, notifications
  • Admin User: Setup with username/password or pre-hashed password for migration
  • Recovery Key: Setup with provided 64-character hex string

Import Order & Dependencies

  1. Recovery key written first (required for repository encryption)
  2. Volumes created
  3. Repositories created (with smart duplicate detection by URL/path)
  4. Notification destinations created
  5. Backup schedules created (with automatic volume mounting, notification and mirror attachments)
  6. Admin user created

Repository Import Safeguards

  • Skips if repository with same URL/path already exists in DB
  • For local repos: auto-detects existing restic repositories on disk
  • Set isExistingRepository: true to import pre-existing restic repositories
  • Detailed warning messages for skipped resources

Migration Support

Changes

New Files

  • config-import.ts - ArkType validation schemas for config import
  • config-import.ts - Core import logic with modular functions
  • import-config.ts - CLI command implementation
  • config-file-import - Complete example with docker-compose, config template, and README

Modified Files

  • backups.dto.ts - Export retentionPolicySchema for reuse
  • index.ts - Register import-config CLI command
  • README.md - Added config file import to examples list

Example Configuration

{
  "recoveryKey": "${RECOVERY_KEY}",
  "volumes": [
    { "name": "my-data", "config": { "backend": "directory", "path": "/data" } }
  ],
  "repositories": [
    { "name": "s3-backup", "config": { "backend": "s3", "bucket": "backups", "accessKeyId": "${AWS_KEY}", "secretAccessKey": "${AWS_SECRET}" } }
  ],
  "backupSchedules": [
    {
      "volume": "my-data",
      "repository": "s3-backup",
      "cronExpression": "0 2 * * *",
      "retentionPolicy": { "keepLast": 7, "keepDaily": 7 },
      "notifications": ["slack-alerts"],
      "mirrors": [{ "repository": "local-mirror" }]
    }
  ],
  "notificationDestinations": [
    { "name": "slack-alerts", "config": { "type": "slack", "webhookUrl": "${SLACK_WEBHOOK}" } }
  ],
  "users": [
    { "username": "admin", "password": "${ADMIN_PASSWORD}" }
  ]
}

CLI Usage

# Import from file
docker compose exec zerobyte bun run cli import-config --config /app/zerobyte.config.json

# Import from stdin
cat config.json | docker compose exec -T zerobyte bun run cli import-config --stdin

# Dry run (validate only)
docker compose exec zerobyte bun run cli import-config --config /app/zerobyte.config.json --dry-run

# JSON output for scripting
docker compose exec zerobyte bun run cli import-config --config /app/zerobyte.config.json --json

Future Possible Enhancements

  • Lock configuration elements against UI changes
  • Visibly tag imported elements in UI
  • Auto-sync between JSON config and UI changes

Related Issues

#13

Related PRs

Import compatible with export files from #92

Summary by CodeRabbit

  • New Features

    • Import full configuration from JSON via a new CLI command (file or stdin) with dry-run and JSON/human-readable output.
    • Option to supply custom short IDs for volumes, repositories, and schedules (validated and checked for collisions).
  • Documentation

    • Added detailed docs, example config, env template, docker-compose sample, and usage examples.
  • Validation & Errors

    • Improved validation, clearer error messages, and pre-checks for existing repositories and resources.

✏️ Tip: You can customize this high-level summary in your review settings.

Copilot AI review requested due to automatic review settings December 1, 2025 11:40
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 1, 2025

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Walkthrough

Adds a config-file import pipeline with validation and apply semantics, a CLI command to import or dry-run JSON configs (file or stdin), ArkType schemas for import formats, example docs and sample files, short-id validation/use across services, new id/error utilities, and a minor auth message text change.

Changes

Cohort / File(s) Summary
Config import core & CLI
app/server/modules/lifecycle/config-import.ts, app/server/cli/commands/import-config.ts, app/server/cli/index.ts
New import module exposing applyConfigImport, validateConfig, and related types; new import-config CLI (file/stdin, dry-run, JSON output) and registration in CLI index.
Schemas & DTO export
app/schemas/config-import.ts, app/server/modules/backups/backups.dto.ts
New ArkType schemas and TypeScript types for import config (volumes, repositories, schedules, notifications, users, recoveryKey); retentionPolicySchema made exported for reuse.
Services: providedShortId & repo existence
app/server/modules/repositories/repositories.service.ts, app/server/modules/backups/backups.service.ts, app/server/modules/volumes/volume.service.ts
createRepository, createSchedule, createVolume accept optional providedShortId; validate via isValidShortId, check uniqueness (ConflictError/BadRequestError), use when valid; repository existence checks adjusted before initialization.
ID & error utilities
app/server/utils/id.ts, app/server/utils/errors.ts
Added centralized SHORT_ID_LENGTH, exported isValidShortId, adjusted generateShortId default; added toError(e: unknown): Error utility.
Auth message
app/server/modules/auth/auth.service.ts
Registration error message changed from "Admin user already exists" to "A user already exists".
Examples & docs
README.md, examples/README.md, examples/config-file-import/README.md
Added documentation and usage guide for config-file import, interpolation, secret handling, CLI examples and options.
Example configs & repo
examples/config-file-import/zerobyte.config.example.json, examples/config-file-import/.env.example, examples/config-file-import/docker-compose.yml, examples/config-file-import/.gitignore
Added example JSON config, env template, docker-compose for import runs, and .gitignore entries (.env, *.json) with exception for zerobyte.config.example.json.

Possibly related PRs

  • nicotsx/zerobyte PR 248 — Modifies backup-schedule shortId handling and tagging logic; likely overlaps schedule short-id changes.
  • nicotsx/zerobyte PR 67 — Broad short-id/system refactor touching utils/id and service callers; overlaps generateShortId/isValidShortId additions.
  • nicotsx/zerobyte PR 103 — Changes backups.service.ts createSchedule logic and persistence; likely interacts with the new providedShortId behavior.

Pre-merge checks

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 17.39% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and concisely describes the main feature addition: JSON configuration import for Infrastructure as Code support, which aligns with the substantial changes across schemas, CLI commands, services, and documentation.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds comprehensive JSON configuration file support to enable Infrastructure as Code (IaC) workflows for Zerobyte. The feature allows users to define volumes, repositories, backup schedules, notification destinations, and admin users in a zerobyte.config.json file with environment variable interpolation for secure secret injection.

Key Changes:

  • Configuration file parsing with environment variable interpolation (${VAR_NAME} syntax)
  • Create-only resource provisioning (existing resources are not overwritten)
  • Support for pre-encrypted secrets from exports and automatic re-encryption of cleartext secrets

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 13 comments.

Show a summary per file
File Description
zerobyte.config.json Example configuration file demonstrating all supported resource types (volumes, repositories, schedules, notifications, admin)
docker-compose.yml Added environment variables and volume mount for config file in production service
app/server/utils/crypto.ts Exported isEncrypted() function to detect already-encrypted values
app/server/modules/repositories/repositories.service.ts Added conditional encryption checks and special handling for existing repositories and local repo migrations
app/server/modules/notifications/notifications.service.ts Added conditional encryption for notification secrets to support importing pre-encrypted values
app/server/modules/lifecycle/startup.ts Core config import logic including file parsing, resource creation, recovery key setup, and admin user provisioning
app/server/modules/backups/backups.service.ts Added duplicate schedule detection to prevent conflicts when importing configurations
README.md Comprehensive documentation of config file format with examples for all resource types

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

You can also share your feedback on Copilot code review for a chance to win a $100 gift card. Take the survey.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (8)
docker-compose.yml (1)

39-45: Consider using a .env file pattern instead of inline placeholder secrets.

The placeholder credentials in the compose file could be accidentally replaced with real secrets and committed. Additionally, this environment block is incomplete—zerobyte.config.json references additional variables like SFTP_PASSWORD, SFTP_PRIVATE_KEY, SLACK_WEBHOOK_URL, EMAIL_PASSWORD, DISCORD_WEBHOOK_URL, GOTIFY_TOKEN, NTFY_PASSWORD, PUSHOVER_USER_KEY, PUSHOVER_API_TOKEN, TELEGRAM_BOT_TOKEN, SHOUTRRR_URL, ADMIN_PASSWORD, and RECOVERY_KEY that are not defined here.

Consider this approach:

    environment:
-      - ACCESS_KEY_ID=your-access-key-id
-      - SECRET_ACCESS_KEY=your-secret-access-key
-      - SMB_PASSWORD=your-smb-password
-      - WEBDAV_PASSWORD=your-webdav-password
-      - GCS_CREDENTIALS=your-gcs-credentials-json
-      - AZURE_KEY=your-azure-key
+      - ACCESS_KEY_ID=${ACCESS_KEY_ID}
+      - SECRET_ACCESS_KEY=${SECRET_ACCESS_KEY}
+      - SMB_PASSWORD=${SMB_PASSWORD}
+      # ... additional vars from .env
+    env_file:
+      - .env

Then provide a .env.example file with all placeholders and add .env to .gitignore.

zerobyte.config.json (1)

133-143: Consider simplifying the example backup schedule.

The example schedule references all 8 notification destinations, which may overwhelm new users. A simpler example with 1-2 destinations would be more approachable while still demonstrating the feature.

-      "notifications": ["slack-alerts", "email-admin", "discord-backups", "gotify-notify", "ntfy-notify", "pushover-notify", "telegram-notify", "custom-shoutrrr"]
+      "notifications": ["slack-alerts"]
app/server/modules/repositories/repositories.service.ts (1)

75-76: Edge case: empty string config.name will bypass ID generation.

If config.name is an empty string "", the condition config.backend === "local" && config.name evaluates to false (correct), but this relies on JavaScript's falsy coercion. Consider an explicit check for clarity.

-	const shortId = (config.backend === "local" && config.name) ? config.name : generateShortId();
+	const shortId = (config.backend === "local" && config.name?.length) ? config.name : generateShortId();
README.md (2)

40-75: Clarify top‑level config structure and JSON validity

The new structure example is helpful, but it currently omits the admin block that you document later. Also, the JSON snippets here use // comments, which are not valid in a real zerobyte.config.json.

Consider:

  • Including an admin example in the top‑level structure.
  • Adding a brief note that comments in the snippets are for documentation only and must be removed in actual JSON files.

337-397: Strengthen warning around recovery key reuse on migration

The “Admin Setup (Automated)” and “About the Recovery Key” sections are clear, but given how critical the recovery key is, it may be worth explicitly stating that when migrating to an existing data directory, the recoveryKey in config must exactly match the one used by that instance, otherwise backups and stored credentials become unrecoverable.

A one‑sentence, explicit “If this value differs from the existing instance’s recovery key, you will lose access to your data” under the migration example would help make the risk unmissable.

app/server/modules/lifecycle/startup.ts (3)

15-47: Config loading and env interpolation are solid; consider handling missing envs more explicitly

The lazy imports and recursive interpolate function are a clean way to support ${VAR_NAME} in arbitrary nested config structures. Right now, missing environment variables silently become empty strings (process.env[v] || ""), which can be hard to diagnose for secrets mis‑spelled in the environment.

Consider (optionally) logging a warning once per missing variable name, or allowing a mode that throws on unresolved ${…} placeholders, so misconfigurations fail fast instead of surfacing later as auth/connectivity errors.


177-207: Tighten admin config validation and logging behavior

The admin auto‑setup block mostly matches the documented behavior (create/import admin only when no users exist, support passwordHash for migrations, and mark hasDownloadedResticPassword when a recoveryKey is present). A couple of minor robustness improvements:

  • The else branch logs "Admin config missing required fields..." even when configFileAdmin is null (i.e., no admin block in the config). That will emit a warning for users who intentionally don’t use config‑driven admin setup. Wrapping the warning in if (configFileAdmin) would avoid noisy logs.
  • If both password and passwordHash are provided, the code silently prefers passwordHash. Given the README says “Use either password OR passwordHash, not both”, you may want to detect that case and log a warning or throw, so misconfigurations are caught early.

These are small behavior tweaks but make the admin import path more predictable.


213-231: Existing remount logic remains compatible with config‑driven mounts

The final auto‑remount block still filters volumes by status and autoRemount, then calls mountVolume for each. With the new config flow, some volumes may already have been mounted earlier when wiring backup schedules, so mountVolume might be called again for the same volume. Assuming mountVolume is idempotent, this is harmless but slightly redundant.

If mountVolume is not strictly idempotent, it may be worth short‑circuiting here for volumes you just mounted during schedule creation.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 999850d and d767346.

📒 Files selected for processing (8)
  • README.md (2 hunks)
  • app/server/modules/backups/backups.service.ts (2 hunks)
  • app/server/modules/lifecycle/startup.ts (2 hunks)
  • app/server/modules/notifications/notifications.service.ts (1 hunks)
  • app/server/modules/repositories/repositories.service.ts (3 hunks)
  • app/server/utils/crypto.ts (2 hunks)
  • docker-compose.yml (2 hunks)
  • zerobyte.config.json (1 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-11-30T15:18:38.895Z
Learnt from: tvarohohlavy
Repo: nicotsx/zerobyte PR: 89
File: app/server/modules/notifications/builders/telegram.ts:3-8
Timestamp: 2025-11-30T15:18:38.895Z
Learning: In app/server/modules/notifications/builders/telegram.ts, the Telegram bot token in Shoutrrr URLs must NOT be URL-encoded. When passed to the Shoutrrr CLI, encoding the bot token (e.g., encoding the colon as %3A) causes "failed to initialize service" errors. The token should be used raw in the URL userinfo section. Chat IDs in the channels query parameter can optionally be encoded but work fine unencoded as well.

Applied to files:

  • app/server/modules/notifications/notifications.service.ts
🧬 Code graph analysis (4)
app/server/modules/backups/backups.service.ts (2)
app/server/db/db.ts (1)
  • db (13-13)
app/server/db/schema.ts (1)
  • backupSchedulesTable (68-95)
app/server/modules/notifications/notifications.service.ts (1)
app/server/utils/crypto.ts (1)
  • cryptoUtils (62-66)
app/server/modules/repositories/repositories.service.ts (3)
app/server/utils/crypto.ts (1)
  • cryptoUtils (62-66)
app/server/utils/id.ts (1)
  • generateShortId (3-6)
app/server/utils/restic.ts (1)
  • restic (736-748)
app/server/modules/lifecycle/startup.ts (6)
app/server/core/scheduler.ts (1)
  • Scheduler (52-52)
app/server/core/constants.ts (1)
  • RESTIC_PASS_FILE (5-5)
app/server/utils/restic.ts (1)
  • restic (736-748)
app/server/modules/volumes/volume.service.ts (1)
  • volumeService (414-427)
app/server/db/schema.ts (4)
  • volumesTable (10-22)
  • repositoriesTable (50-62)
  • notificationDestinationsTable (112-120)
  • usersTable (28-35)
app/server/modules/auth/auth.service.ts (2)
  • hasUsers (146-149)
  • authService (179-179)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Agent
🔇 Additional comments (12)
app/server/utils/crypto.ts (1)

8-10: LGTM! Clean extraction of encryption check logic.

This helper function correctly encapsulates the encryption detection logic that was previously inline in encrypt(). The type guard (typeof val === "string") handles undefined/null inputs safely.

app/server/modules/notifications/notifications.service.ts (1)

36-81: LGTM! Encryption guards correctly prevent double-encryption.

The conditional encryption pattern is consistently applied across all notification types. The ntfy.password handling at line 61 correctly preserves the optional field semantics with the nested check.

app/server/modules/backups/backups.service.ts (1)

80-89: LGTM! Idempotent schedule creation with proper duplicate detection.

The duplicate check correctly prevents creating multiple schedules for the same volume-repository pair, supporting the create-only semantics described in the PR objectives. The error message is descriptive and includes both entity names.

docker-compose.yml (1)

49-50: Mounting ./mydata as read-only may conflict with backup restore operations.

The ./mydata:/mydata:ro mount is read-only, which is appropriate for backup sources but will prevent restores to that volume. Verify this is intentional for the example configuration.

zerobyte.config.json (1)

226-230: LGTM! Admin configuration with proper secret handling.

The admin section correctly uses environment variable interpolation for sensitive values (password and recoveryKey), allowing secure configuration without hardcoding secrets.

app/server/modules/repositories/repositories.service.ts (2)

21-23: LGTM! Encryption guard for customPassword.

The optional check is correct—customPassword may not exist on all configs.


114-124: Good resilience: graceful handling of pre-existing repositories.

This fallback logic correctly handles the case where a repository already exists (e.g., during re-deployment or config re-import) by verifying accessibility via snapshots() instead of failing immediately.

README.md (2)

77-331: Config examples for volumes/repos/schedules/notifications look coherent

The examples for volume/repository types, backup schedules (including notification wiring), and notification destinations line up well with the described behavior and env‑var interpolation model. They give enough detail to author real configs for most backends without guessing.


394-417: docker‑compose snippet aligns with startup behavior

Mounting ./zerobyte.config.json into /app/zerobyte.config.json matches the default path resolution in startup.ts (process.cwd() is /app in the container), so this example should “just work” without needing ZEROBYTE_CONFIG_PATH. Looks good.

app/server/modules/lifecycle/startup.ts (3)

99-175: Backup schedule creation and notification assignment flow is well‑structured

The schedule import logic correctly:

  • Resolves volumes and repositories by name (volume/volumeName, repository/repositoryName).
  • Mounts volumes on demand if they are not yet mounted.
  • Creates schedules via the backups service with the resolved IDs.
  • Optionally resolves and assigns notification destinations by name, supporting both simple string names and detailed objects with notifyOn* flags and sensible defaults.

The per‑item try/catch blocks and “continue on failure” behavior give good resilience during partial imports.


68-98: Verify notification destination creation API and type handling

The notification destination creation call may be missing the type argument:

await notificationsServiceModule.notificationsService.createDestination(n.name, n.config);

If the service API follows the same pattern as volume and repository creation (which pass name, type, and config as separate arguments), this call should include n.type. Confirm the actual createDestination method signature and whether type needs to be passed explicitly or is inferred from n.config.


52-62: Avoid unconditionally overwriting RESTIC_PASS_FILE when recoveryKey is provided

This block always writes configFileAdmin.recoveryKey to RESTIC_PASS_FILE whenever it's present in the config, without checking whether the file already exists. On an existing instance with a prior recovery key, this could rotate the key and render existing backup data unreadable.

Consider:

  • Only write the file if it does not already exist, or
  • Check for an existing file and either verify it matches the configured recoveryKey or log a clear error and skip writing

Regarding the await import("../../core/constants.js") path with the .js extension: verify this is consistent with your module system and TypeScript configuration, as most other imports in the codebase appear to omit extensions.

@nicotsx
Copy link
Owner

nicotsx commented Dec 1, 2025

Hello, I appreciate your contribution however I cannot afford to spend the time reviewing it as it has been AI generated. Were you able to test the implementation?

@tvarohohlavy
Copy link
Contributor Author

Hello, I appreciate your contribution however I cannot afford to spend the time reviewing it as it has been AI generated. Were you able to test the implementation?

Hi,
I will obviously first fix the suggestions above and correct discrepancies against the other PR that is complementary to this, where I have already fixed most of the suggestions.

Lots of the discrepancies cones from me splitting my code that was for both import and export at the same time, but it made sense to separate it into 2 PRs.

I did a lot of tests importing and exporting, both with fresh instances and recovering old ones, but I did not test all possible repository and volume configurations, because its a lot and I dont have access to all at the moment.

Yes, I use Github Copilot to help me and my main languages are Python, C++ and Golang however I spent on it several hours iterating a slowly bringing more features and fixing what AI has suggested. Also I did designed my changes and come up with the ideas and edge cases.

I really like Zerobyte and see its wast potential, but configuration through UI only is definitely its downside, so I wanted to make it better.

Thanks for all your good work!

@nicotsx
Copy link
Owner

nicotsx commented Dec 1, 2025

Okay thanks for the clarification. I'll give you my comments when I have some time

@nicotsx nicotsx added the AI Most of the code was generated by AI label Dec 2, 2025
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (1)
app/server/modules/repositories/repositories.service.ts (1)

53-57: Missing encryption guard for SFTP password field.

The SFTP backend only encrypts privateKey, but some SFTP configurations may use password authentication. If the schema supports a password field for SFTP, it should also be guarded.

Apply this diff to add password encryption for SFTP:

 		case "sftp":
 			if (!cryptoUtils.isEncrypted(config.privateKey)) {
 				encryptedConfig.privateKey = await cryptoUtils.encrypt(config.privateKey);
 			}
+			if (config.password && !cryptoUtils.isEncrypted(config.password)) {
+				encryptedConfig.password = await cryptoUtils.encrypt(config.password);
+			}
 			break;
🧹 Nitpick comments (1)
app/server/modules/lifecycle/startup.ts (1)

22-23: Duplicate dynamic imports of fs.

The node:fs/promises module is imported dynamically twice (lines 22-23 and 59-60). Consider importing it once at the top of the function or reusing the first import.

 	try {
-		const fs = await import("node:fs/promises");
-		const { RESTIC_PASS_FILE } = await import("../../core/constants.js");
+		const { RESTIC_PASS_FILE } = await import("../../core/constants");
 		if (configFileAdmin && configFileAdmin.recoveryKey) {

Note: The fs variable from line 22 can be reused here if moved to a broader scope, and the .js extension on the constants import may be unnecessary depending on your TypeScript configuration.

Also applies to: 59-60

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d767346 and 1de82fa.

📒 Files selected for processing (5)
  • README.md (2 hunks)
  • app/server/modules/lifecycle/startup.ts (2 hunks)
  • app/server/modules/repositories/repositories.service.ts (3 hunks)
  • docker-compose.yml (2 hunks)
  • zerobyte.config.json (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
app/server/modules/lifecycle/startup.ts (6)
app/server/core/scheduler.ts (1)
  • Scheduler (52-52)
app/server/core/constants.ts (1)
  • RESTIC_PASS_FILE (5-5)
app/server/utils/restic.ts (1)
  • restic (736-748)
app/server/db/db.ts (1)
  • db (13-13)
app/server/db/schema.ts (4)
  • volumesTable (10-22)
  • repositoriesTable (50-62)
  • notificationDestinationsTable (112-120)
  • usersTable (28-35)
app/server/modules/auth/auth.service.ts (1)
  • hasUsers (146-149)
app/server/modules/repositories/repositories.service.ts (5)
app/server/utils/crypto.ts (1)
  • cryptoUtils (62-66)
app/server/db/db.ts (1)
  • db (13-13)
app/server/db/schema.ts (1)
  • repositoriesTable (50-62)
app/server/utils/id.ts (1)
  • generateShortId (3-6)
app/server/utils/restic.ts (1)
  • restic (736-748)
🔇 Additional comments (9)
docker-compose.yml (1)

39-67: Environment and volume configuration looks complete.

The environment block provides all variables referenced in the example zerobyte.config.json, and the volume mappings correctly mount the config file read-only. The placeholder values are appropriate for an example template.

zerobyte.config.json (1)

1-217: Well-structured example configuration.

The configuration file correctly demonstrates all supported resource types with appropriate field structures. Previous review issues have been addressed:

  • SFTP repository uses user and privateKey (lines 113-114)
  • Notification destinations have type inside config (e.g., lines 134-135)
  • Environment variable interpolation syntax is consistent throughout
app/server/modules/lifecycle/startup.ts (2)

191-225: Admin setup logic properly handles migration scenarios.

The implementation correctly:

  • Validates mutual exclusivity of password and passwordHash (lines 194-197)
  • Only creates admin when no users exist (line 199)
  • Supports password hash import for migrations (lines 201-208)
  • Marks recovery key as acknowledged when provided via config (lines 215-217)

155-187: Flexible notification assignment design.

The implementation elegantly handles both simple string references (["slack-alerts"]) and detailed objects with granular control over notification triggers. Defaulting all triggers to true when using string references is a sensible user-friendly choice.

app/server/modules/repositories/repositories.service.ts (2)

76-89: Good migration support with conflict detection.

The shortId logic correctly allows users to specify an existing repository name for migration scenarios while preventing conflicts. The explicit error message on line 85 clearly explains the shortId's purpose as a subdirectory name.


127-137: Appropriate fallback for existing repository verification.

When restic init fails with "config file already exists", attempting verification via snapshots() is a reasonable approach to determine if the repository is valid and accessible. This handles the edge case where a user re-imports a configuration.

README.md (3)

40-56: Clear documentation of create-only semantics.

The info box clearly explains that the config file serves as "initial setup" rather than "desired state sync," which is important for users to understand. This prevents confusion about config file behavior on subsequent restarts.


239-344: Notification examples correctly structured.

The notification destination examples now show the type field inside the config object (e.g., lines 246-252 for Slack), which matches the expected schema structure.


385-404: Excellent recovery key documentation.

The warning box clearly explains the dual purpose of the recovery key (Restic password + database encryption), provides generation commands for multiple platforms, and shows how to retrieve it from an existing instance. This is critical information for data recovery.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/server/modules/repositories/repositories.service.ts (1)

25-48: Missing isEncrypted() guards will cause double-encryption of imported secrets.

The PR objectives state support for "detection of encrypted values via cryptoUtils.isEncrypted()" and importing both encrypted and cleartext secrets. However, encryptConfig unconditionally encrypts all sensitive fields without checking if they're already encrypted. When importing pre-encrypted values, this will corrupt the data by double-encrypting.

Apply guards to all sensitive field encryption:

 	switch (config.backend) {
 		case "s3":
 		case "r2":
-				encryptedConfig.accessKeyId = await cryptoUtils.encrypt(config.accessKeyId);
-				encryptedConfig.secretAccessKey = await cryptoUtils.encrypt(config.secretAccessKey);
+			if (!cryptoUtils.isEncrypted(config.accessKeyId)) {
+				encryptedConfig.accessKeyId = await cryptoUtils.encrypt(config.accessKeyId);
+			}
+			if (!cryptoUtils.isEncrypted(config.secretAccessKey)) {
+				encryptedConfig.secretAccessKey = await cryptoUtils.encrypt(config.secretAccessKey);
+			}
 			break;
 		case "gcs":
-				encryptedConfig.credentialsJson = await cryptoUtils.encrypt(config.credentialsJson);
+			if (!cryptoUtils.isEncrypted(config.credentialsJson)) {
+				encryptedConfig.credentialsJson = await cryptoUtils.encrypt(config.credentialsJson);
+			}
 			break;
 		case "azure":
-				encryptedConfig.accountKey = await cryptoUtils.encrypt(config.accountKey);
+			if (!cryptoUtils.isEncrypted(config.accountKey)) {
+				encryptedConfig.accountKey = await cryptoUtils.encrypt(config.accountKey);
+			}
 			break;
 		case "rest":
 			if (config.username) {
-				encryptedConfig.username = await cryptoUtils.encrypt(config.username);
+				if (!cryptoUtils.isEncrypted(config.username)) {
+					encryptedConfig.username = await cryptoUtils.encrypt(config.username);
+				}
 			}
 			if (config.password) {
-				encryptedConfig.password = await cryptoUtils.encrypt(config.password);
+				if (!cryptoUtils.isEncrypted(config.password)) {
+					encryptedConfig.password = await cryptoUtils.encrypt(config.password);
+				}
 			}
 			break;
 		case "sftp":
-				encryptedConfig.privateKey = await cryptoUtils.encrypt(config.privateKey);
+			if (!cryptoUtils.isEncrypted(config.privateKey)) {
+				encryptedConfig.privateKey = await cryptoUtils.encrypt(config.privateKey);
+			}
 			break;
 	}

Also apply the same pattern to customPassword (lines 21-23):

 	if (config.customPassword) {
-		encryptedConfig.customPassword = await cryptoUtils.encrypt(config.customPassword);
+		if (!cryptoUtils.isEncrypted(config.customPassword)) {
+			encryptedConfig.customPassword = await cryptoUtils.encrypt(config.customPassword);
+		}
 	}
🧹 Nitpick comments (1)
app/server/modules/repositories/repositories.service.ts (1)

117-127: String-based error detection is fragile but acceptable.

The error message matching ("config file already exists") could break if restic changes its error messages in future versions. The verification fallback via snapshots() is a reasonable approach to determine if the repository is actually usable.

Consider extracting the error string to a constant for easier maintenance, or adding a comment noting the restic version this was tested against.

+// Error message from restic when repository already initialized
+const RESTIC_CONFIG_EXISTS_ERROR = "config file already exists";
+
 		if (errorStr.includes("config file already exists")) {
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1de82fa and abe6eb7.

📒 Files selected for processing (1)
  • app/server/modules/repositories/repositories.service.ts (4 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
app/server/modules/repositories/repositories.service.ts (5)
app/server/utils/crypto.ts (1)
  • cryptoUtils (58-61)
app/server/db/db.ts (1)
  • db (13-13)
app/server/db/schema.ts (1)
  • repositoriesTable (50-62)
app/server/utils/id.ts (1)
  • generateShortId (3-6)
app/server/utils/restic.ts (1)
  • restic (736-748)
🔇 Additional comments (1)
app/server/modules/repositories/repositories.service.ts (1)

65-79: LGTM!

The conflict check for user-provided shortId values addresses the previous review concern. The logic properly validates uniqueness before using config.name for migration scenarios, with a clear error message explaining the constraint.

@tvarohohlavy
Copy link
Contributor Author

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)

app/server/modules/repositories/repositories.service.ts (1)> 25-48: Missing isEncrypted() guards will cause double-encryption of imported secrets.

The PR objectives state support for "detection of encrypted values via cryptoUtils.isEncrypted()" and importing both encrypted and cleartext secrets. However, encryptConfig unconditionally encrypts all sensitive fields without checking if they're already encrypted. When importing pre-encrypted values, this will corrupt the data by double-encrypting.
Apply guards to all sensitive field encryption:

 	switch (config.backend) {
 		case "s3":
 		case "r2":
-				encryptedConfig.accessKeyId = await cryptoUtils.encrypt(config.accessKeyId);
-				encryptedConfig.secretAccessKey = await cryptoUtils.encrypt(config.secretAccessKey);
+			if (!cryptoUtils.isEncrypted(config.accessKeyId)) {
+				encryptedConfig.accessKeyId = await cryptoUtils.encrypt(config.accessKeyId);
+			}
+			if (!cryptoUtils.isEncrypted(config.secretAccessKey)) {
+				encryptedConfig.secretAccessKey = await cryptoUtils.encrypt(config.secretAccessKey);
+			}
 			break;
 		case "gcs":
-				encryptedConfig.credentialsJson = await cryptoUtils.encrypt(config.credentialsJson);
+			if (!cryptoUtils.isEncrypted(config.credentialsJson)) {
+				encryptedConfig.credentialsJson = await cryptoUtils.encrypt(config.credentialsJson);
+			}
 			break;
 		case "azure":
-				encryptedConfig.accountKey = await cryptoUtils.encrypt(config.accountKey);
+			if (!cryptoUtils.isEncrypted(config.accountKey)) {
+				encryptedConfig.accountKey = await cryptoUtils.encrypt(config.accountKey);
+			}
 			break;
 		case "rest":
 			if (config.username) {
-				encryptedConfig.username = await cryptoUtils.encrypt(config.username);
+				if (!cryptoUtils.isEncrypted(config.username)) {
+					encryptedConfig.username = await cryptoUtils.encrypt(config.username);
+				}
 			}
 			if (config.password) {
-				encryptedConfig.password = await cryptoUtils.encrypt(config.password);
+				if (!cryptoUtils.isEncrypted(config.password)) {
+					encryptedConfig.password = await cryptoUtils.encrypt(config.password);
+				}
 			}
 			break;
 		case "sftp":
-				encryptedConfig.privateKey = await cryptoUtils.encrypt(config.privateKey);
+			if (!cryptoUtils.isEncrypted(config.privateKey)) {
+				encryptedConfig.privateKey = await cryptoUtils.encrypt(config.privateKey);
+			}
 			break;
 	}

Also apply the same pattern to customPassword (lines 21-23):

 	if (config.customPassword) {
-		encryptedConfig.customPassword = await cryptoUtils.encrypt(config.customPassword);
+		if (!cryptoUtils.isEncrypted(config.customPassword)) {
+			encryptedConfig.customPassword = await cryptoUtils.encrypt(config.customPassword);
+		}
 	}

🧹 Nitpick comments (1)

app/server/modules/repositories/repositories.service.ts (1)> 117-127: String-based error detection is fragile but acceptable.

The error message matching ("config file already exists") could break if restic changes its error messages in future versions. The verification fallback via snapshots() is a reasonable approach to determine if the repository is actually usable.
Consider extracting the error string to a constant for easier maintenance, or adding a comment noting the restic version this was tested against.

+// Error message from restic when repository already initialized
+const RESTIC_CONFIG_EXISTS_ERROR = "config file already exists";
+
 		if (errorStr.includes("config file already exists")) {

📜 Review details

Re-encryption is not possible due to this in existing code:

if (data.startsWith(encryptionPrefix)) {

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (1)
app/server/modules/repositories/repositories.service.ts (1)

18-50: Avoid double-encrypting repository secrets by guarding with cryptoUtils.isEncrypted.

As written, encryptConfig will attempt to encrypt all sensitive fields unconditionally. For JSON-config imports that may already contain encrypted values (migration scenarios), this will double-encrypt secrets, breaking access to existing repositories. This is the same issue maintainers already raised for this helper and needs to be fixed here, not just in higher-level import code.

You can make encryptConfig idempotent by checking cryptoUtils.isEncrypted before encrypting each field:

 const encryptConfig = async (config: RepositoryConfig): Promise<RepositoryConfig> => {
 	const encryptedConfig: Record<string, string | boolean | number> = { ...config };

-	if (config.customPassword) {
-		encryptedConfig.customPassword = await cryptoUtils.encrypt(config.customPassword);
-	}
+	if (config.customPassword && !cryptoUtils.isEncrypted(config.customPassword)) {
+		encryptedConfig.customPassword = await cryptoUtils.encrypt(config.customPassword);
+	}

 	switch (config.backend) {
 		case "s3":
 		case "r2":
-			encryptedConfig.accessKeyId = await cryptoUtils.encrypt(config.accessKeyId);
-			encryptedConfig.secretAccessKey = await cryptoUtils.encrypt(config.secretAccessKey);
+			if (!cryptoUtils.isEncrypted(config.accessKeyId)) {
+				encryptedConfig.accessKeyId = await cryptoUtils.encrypt(config.accessKeyId);
+			}
+			if (!cryptoUtils.isEncrypted(config.secretAccessKey)) {
+				encryptedConfig.secretAccessKey = await cryptoUtils.encrypt(config.secretAccessKey);
+			}
 			break;
 		case "gcs":
-			encryptedConfig.credentialsJson = await cryptoUtils.encrypt(config.credentialsJson);
+			if (!cryptoUtils.isEncrypted(config.credentialsJson)) {
+				encryptedConfig.credentialsJson = await cryptoUtils.encrypt(config.credentialsJson);
+			}
 			break;
 		case "azure":
-			encryptedConfig.accountKey = await cryptoUtils.encrypt(config.accountKey);
+			if (!cryptoUtils.isEncrypted(config.accountKey)) {
+				encryptedConfig.accountKey = await cryptoUtils.encrypt(config.accountKey);
+			}
 			break;
 		case "rest":
-			if (config.username) {
-				encryptedConfig.username = await cryptoUtils.encrypt(config.username);
-			}
-			if (config.password) {
-				encryptedConfig.password = await cryptoUtils.encrypt(config.password);
-			}
+			if (config.username && !cryptoUtils.isEncrypted(config.username)) {
+				encryptedConfig.username = await cryptoUtils.encrypt(config.username);
+			}
+			if (config.password && !cryptoUtils.isEncrypted(config.password)) {
+				encryptedConfig.password = await cryptoUtils.encrypt(config.password);
+			}
 			break;
 		case "sftp":
-			encryptedConfig.privateKey = await cryptoUtils.encrypt(config.privateKey);
+			if (!cryptoUtils.isEncrypted(config.privateKey)) {
+				encryptedConfig.privateKey = await cryptoUtils.encrypt(config.privateKey);
+			}
 			break;
 	}

 	return encryptedConfig as RepositoryConfig;
 };

This keeps encryptConfig safe to call for both fresh API-created repos and config-driven imports that may mix cleartext and already-encrypted secrets.

🧹 Nitpick comments (1)
app/server/modules/repositories/repositories.service.ts (1)

88-105: Repo-existence gating is sound; consider optionally surfacing snapshot errors.

The repoExists check via restic.snapshots(encryptedConfig) combined with the isExistingRepository gates correctly prevents accidental init over an existing repo and enforces that imports only succeed when the repository is actually accessible.

One optional improvement: the .catch(() => false) currently swallows all restic errors. Emitting a debug/log entry with toMessage(error) here (or just before throwing the “Cannot access existing repository...” error) would make diagnosing misconfigured paths/credentials easier without changing behavior.

Also applies to: 125-128

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between abe6eb7 and 1d42630.

📒 Files selected for processing (1)
  • app/server/modules/repositories/repositories.service.ts (3 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-12-03T19:48:55.097Z
Learnt from: nicotsx
Repo: nicotsx/zerobyte PR: 95
File: app/server/modules/backups/backups.service.ts:525-533
Timestamp: 2025-12-03T19:48:55.097Z
Learning: In app/server/modules/backups/backups.service.ts, the mirror copy operations use shared locks (acquireShared) for both source and mirror repositories because restic supports parallel write operations and concurrent backups with non-exclusive locks. Only maintenance operations like prune and rebuild-index require exclusive locks in restic.

Applied to files:

  • app/server/modules/repositories/repositories.service.ts
🧬 Code graph analysis (1)
app/server/modules/repositories/repositories.service.ts (3)
app/server/db/schema.ts (1)
  • repositoriesTable (50-62)
app/server/utils/id.ts (1)
  • generateShortId (3-6)
app/server/utils/restic.ts (1)
  • restic (736-748)
🔇 Additional comments (1)
app/server/modules/repositories/repositories.service.ts (1)

65-84: Local shortId / config.name handling for migrations looks consistent.

Using config.name as shortId only for backend === "local" (with a DB uniqueness check on repositoriesTable.shortId) and then normalizing processedConfig to { ...config, name: shortId } for locals cleanly supports the “import existing local repo by directory name” scenario without disturbing other backends. I don’t see functional issues with this approach.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/server/modules/repositories/repositories.service.ts (1)

19-52: Critical: Missing encryption guards will cause double-encryption of already-encrypted values.

The encryptConfig function unconditionally encrypts sensitive fields without checking if they're already encrypted. When importing from an exported config (which already contains encrypted values), this will corrupt the credentials by double-encrypting them.

This was explicitly flagged in the PR review comments. Each sensitive field needs an isEncrypted guard:

 const encryptConfig = async (config: RepositoryConfig): Promise<RepositoryConfig> => {
 	const encryptedConfig: Record<string, string | boolean | number> = { ...config };

-	if (config.customPassword) {
+	if (config.customPassword && !cryptoUtils.isEncrypted(config.customPassword)) {
 		encryptedConfig.customPassword = await cryptoUtils.encrypt(config.customPassword);
 	}

 	switch (config.backend) {
 		case "s3":
 		case "r2":
-			encryptedConfig.accessKeyId = await cryptoUtils.encrypt(config.accessKeyId);
-			encryptedConfig.secretAccessKey = await cryptoUtils.encrypt(config.secretAccessKey);
+			if (!cryptoUtils.isEncrypted(config.accessKeyId)) {
+				encryptedConfig.accessKeyId = await cryptoUtils.encrypt(config.accessKeyId);
+			}
+			if (!cryptoUtils.isEncrypted(config.secretAccessKey)) {
+				encryptedConfig.secretAccessKey = await cryptoUtils.encrypt(config.secretAccessKey);
+			}
 			break;
 		case "gcs":
-			encryptedConfig.credentialsJson = await cryptoUtils.encrypt(config.credentialsJson);
+			if (!cryptoUtils.isEncrypted(config.credentialsJson)) {
+				encryptedConfig.credentialsJson = await cryptoUtils.encrypt(config.credentialsJson);
+			}
 			break;
 		case "azure":
-			encryptedConfig.accountKey = await cryptoUtils.encrypt(config.accountKey);
+			if (!cryptoUtils.isEncrypted(config.accountKey)) {
+				encryptedConfig.accountKey = await cryptoUtils.encrypt(config.accountKey);
+			}
 			break;
 		case "rest":
-			if (config.username) {
+			if (config.username && !cryptoUtils.isEncrypted(config.username)) {
 				encryptedConfig.username = await cryptoUtils.encrypt(config.username);
 			}
-			if (config.password) {
+			if (config.password && !cryptoUtils.isEncrypted(config.password)) {
 				encryptedConfig.password = await cryptoUtils.encrypt(config.password);
 			}
 			break;
 		case "sftp":
-			encryptedConfig.privateKey = await cryptoUtils.encrypt(config.privateKey);
+			if (!cryptoUtils.isEncrypted(config.privateKey)) {
+				encryptedConfig.privateKey = await cryptoUtils.encrypt(config.privateKey);
+			}
 			break;
 	}

 	return encryptedConfig as RepositoryConfig;
 };
🧹 Nitpick comments (4)
app/server/modules/backups/backups.service.ts (1)

90-99: LGTM! Duplicate schedule prevention correctly implemented.

The check for existing schedules with the same volume and repository is a sensible safeguard for the config-driven startup flow.

Minor note: Line 98 exceeds the 120 character line width specified in coding guidelines.

 	if (existingSchedule) {
-		throw new ConflictError(`A backup schedule for volume '${volume.name}' and repository '${repository.name}' already exists`);
+		throw new ConflictError(
+			`A backup schedule for volume '${volume.name}' and repository '${repository.name}' already exists`
+		);
 	}
docker-compose.yml (1)

62-62: Recovery key placeholder should indicate format requirements.

The placeholder your-64-char-hex-recovery-key is helpful, but users might not realize this must be exactly 64 hex characters. Consider using a realistic example format:

-      - RECOVERY_KEY=your-64-char-hex-recovery-key
+      - RECOVERY_KEY=0000000000000000000000000000000000000000000000000000000000000000

Or add a comment indicating how to generate one (as documented in README).

app/server/modules/lifecycle/startup.ts (2)

29-44: Add TypeScript type annotation to the interpolate function.

The interpolate function lacks type annotations, which reduces type safety and IDE support:

-		function interpolate(obj) {
+		function interpolate<T>(obj: T): T {
 			if (typeof obj === "string") {
-				return obj.replace(/\$\{([^}]+)\}/g, (_, v) => {
+				return obj.replace(/\$\{([^}]+)\}/g, (_, v: string) => {
 					if (process.env[v] === undefined) {
 						logger.warn(`Environment variable '${v}' is not defined. Replacing with empty string.`);
 						return "";
 					}
-					return process.env[v];
+					return process.env[v] as string;
 				});
 			} else if (Array.isArray(obj)) {
-				return obj.map(interpolate);
+				return obj.map(interpolate) as T;
 			} else if (obj && typeof obj === "object") {
-				return Object.fromEntries(Object.entries(obj).map(([k, v]) => [k, interpolate(v)]));
+				return Object.fromEntries(Object.entries(obj).map(([k, v]) => [k, interpolate(v)])) as T;
 			}
-			return obj;
+			return obj as T;
 		}

113-153: Consider validating schedule name uniqueness before creation.

The schedule creation loop mounts volumes as needed, which is good. However, if a schedule fails due to name conflict (handled in backupsService.createSchedule), the volume will have been mounted unnecessarily. This is minor but could be optimized by checking for existing schedules first.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1d42630 and ea9003c.

📒 Files selected for processing (5)
  • README.md (2 hunks)
  • app/server/modules/backups/backups.service.ts (1 hunks)
  • app/server/modules/lifecycle/startup.ts (2 hunks)
  • app/server/modules/repositories/repositories.service.ts (3 hunks)
  • docker-compose.yml (2 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx,js,jsx}: Use Biome for formatting and linting with configuration: tabs (not spaces), 120 character line width, double quotes
Do not auto-organize imports in Biome - imports organization is disabled in the codebase

Files:

  • app/server/modules/backups/backups.service.ts
  • app/server/modules/lifecycle/startup.ts
  • app/server/modules/repositories/repositories.service.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

All imports must include file extensions when targeting Node/Bun (TypeScript with "type": "module")

Files:

  • app/server/modules/backups/backups.service.ts
  • app/server/modules/lifecycle/startup.ts
  • app/server/modules/repositories/repositories.service.ts
🧠 Learnings (7)
📓 Common learnings
Learnt from: CR
Repo: nicotsx/zerobyte PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-04T17:31:30.248Z
Learning: Repository backend configurations must be defined with schemas in `app/schemas/restic.ts`, URL construction in `buildRepoUrl()`, and environment setup in `buildEnv()`
📚 Learning: 2025-12-04T17:31:30.248Z
Learnt from: CR
Repo: nicotsx/zerobyte PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-04T17:31:30.248Z
Learning: Repository backend configurations must be defined with schemas in `app/schemas/restic.ts`, URL construction in `buildRepoUrl()`, and environment setup in `buildEnv()`

Applied to files:

  • app/server/modules/lifecycle/startup.ts
  • app/server/modules/repositories/repositories.service.ts
📚 Learning: 2025-12-04T17:31:30.248Z
Learnt from: CR
Repo: nicotsx/zerobyte PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-04T17:31:30.248Z
Learning: Applies to app/server/modules/backends/*/!(backend).ts : Volume backend implementations must extend the base backend pattern with `mount()` and `unmount()` methods

Applied to files:

  • app/server/modules/lifecycle/startup.ts
📚 Learning: 2025-12-04T17:31:30.248Z
Learnt from: CR
Repo: nicotsx/zerobyte PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-04T17:31:30.248Z
Learning: Applies to app/server/utils/restic.ts : Restic password file must have 0600 permissions and never be exposed in logs or error messages

Applied to files:

  • app/server/modules/lifecycle/startup.ts
  • app/server/modules/repositories/repositories.service.ts
📚 Learning: 2025-12-04T17:31:30.248Z
Learnt from: CR
Repo: nicotsx/zerobyte PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-04T17:31:30.248Z
Learning: Applies to **/*.{ts,tsx,js,jsx} : Use Biome for formatting and linting with configuration: tabs (not spaces), 120 character line width, double quotes

Applied to files:

  • app/server/modules/lifecycle/startup.ts
📚 Learning: 2025-12-03T19:48:55.097Z
Learnt from: nicotsx
Repo: nicotsx/zerobyte PR: 95
File: app/server/modules/backups/backups.service.ts:525-533
Timestamp: 2025-12-03T19:48:55.097Z
Learning: In app/server/modules/backups/backups.service.ts, the mirror copy operations use shared locks (acquireShared) for both source and mirror repositories because restic supports parallel write operations and concurrent backups with non-exclusive locks. Only maintenance operations like prune and rebuild-index require exclusive locks in restic.

Applied to files:

  • app/server/modules/repositories/repositories.service.ts
📚 Learning: 2025-12-04T17:31:30.248Z
Learnt from: CR
Repo: nicotsx/zerobyte PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-04T17:31:30.248Z
Learning: Volume mount paths are located at `/var/lib/zerobyte/mounts/<volume-name>` and local repository paths at `/var/lib/zerobyte/repositories/<repo-name>`

Applied to files:

  • README.md
🧬 Code graph analysis (3)
app/server/modules/backups/backups.service.ts (2)
app/server/db/db.ts (1)
  • db (14-14)
app/server/db/schema.ts (1)
  • backupSchedulesTable (68-97)
app/server/modules/lifecycle/startup.ts (3)
app/server/core/constants.ts (1)
  • RESTIC_PASS_FILE (5-5)
app/server/db/schema.ts (4)
  • volumesTable (10-22)
  • repositoriesTable (50-62)
  • notificationDestinationsTable (116-124)
  • usersTable (28-35)
app/server/modules/auth/auth.service.ts (1)
  • hasUsers (146-149)
app/server/modules/repositories/repositories.service.ts (5)
app/server/core/config.ts (1)
  • config (21-21)
app/server/db/db.ts (1)
  • db (14-14)
app/server/db/schema.ts (1)
  • repositoriesTable (50-62)
app/server/utils/id.ts (1)
  • generateShortId (3-6)
app/server/utils/restic.ts (1)
  • restic (805-818)
🔇 Additional comments (10)
docker-compose.yml (1)

66-67: LGTM! Config file and data volume mounts are correctly configured.

The read-only mount for the config file (:ro) is appropriate since it should not be modified by the container.

app/server/modules/repositories/repositories.service.ts (2)

66-80: LGTM! Local repository shortId handling with conflict detection.

The logic correctly handles migration scenarios where config.name is provided for local backends, with proper conflict checking against existing repositories.


89-105: LGTM! Repository existence validation is well-implemented.

The logic correctly:

  1. Detects if a restic repo already exists at the location
  2. Prevents accidental overwrite without explicit isExistingRepository flag
  3. Validates that imported repos actually exist and are accessible
app/server/modules/lifecycle/startup.ts (3)

58-76: LGTM! Recovery key validation is thorough and secure.

The validation correctly:

  1. Checks type, length, and hex format
  2. Writes with restrictive permissions (0o600)
  3. Logs success without exposing the key value

82-111: LGTM! Resource creation with appropriate error isolation.

Each resource type (volumes, repositories, notifications) is created independently with try-catch, preventing one failure from blocking others. Warnings are logged with helpful context.


191-225: LGTM! Admin setup with proper validation and migration support.

The implementation correctly:

  1. Validates mutual exclusivity of password and passwordHash
  2. Only creates admin when no users exist
  3. Supports migration with pre-hashed passwords
  4. Marks recovery key as downloaded when provided via config
README.md (4)

40-56: LGTM! Clear explanation of create-only semantics.

The documentation clearly explains that the config file serves as "initial setup" rather than "desired state sync", which sets correct expectations for users.


154-168: LGTM! Local repository migration documentation is helpful.

The note about importing existing local repositories with config.name and isExistingRepository is well-explained and aligns with the implementation in repositories.service.ts.


385-405: LGTM! Recovery key documentation is comprehensive and security-focused.

The warning about losing access to backups and credentials, along with generation commands for multiple platforms, is excellent user guidance.


430-430: LGTM! Config file mount instruction added to example.

The read-only mount for the config file is correctly documented.

@tvarohohlavy
Copy link
Contributor Author

I need to check what changes will be needed regards the schedule naming change

@nicotsx
Copy link
Owner

nicotsx commented Dec 6, 2025

I was working earlier this week on the reset password feature and I started implementing a command pattern. I think this could be applied here as well. In order to avoid loading the stuff at startup and have all this runtime code the import feature would live only in 1 separated file and be executed like this:

docker run -it --rm -v /var/lib/zerobyte:/var/lib/zerobyte ghcr.io/nicotsx/zerobyte import-config

This would load automatically the file from /var/lib/zerobyte/data/import.json for example

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (4)
README.md (1)

154-168: Clarify the relationship between config.name and subdirectory naming.

The documentation refers to config.name but this field is actually used as the shortId (subdirectory name) for local repositories. The distinction between the repository's display name (lines 159, 168) and config.name could be clearer.

Consider adding: "The config.name value becomes the subdirectory name under the specified path, while name at the top level is the display name for the repository in the UI."

docker-compose.yml (2)

42-65: Consider adding a security reminder comment.

While placeholder values are appropriate for example configurations, it would be helpful to add a comment warning users not to commit real credentials to version control.

Consider adding above the environment section:

    # WARNING: Replace placeholder values with real credentials
    # Do not commit real secrets to version control
    # Consider using .env files or Docker secrets for production
    environment:

69-70: Document that these are example paths.

The mounted paths ./zerobyte.config.json and ./mydata are examples and won't exist by default. Consider adding a comment to clarify these should be customized:

      - ./zerobyte.config.json:/app/zerobyte.config.json:ro  # Create and customize this config file
      - ./mydata:/mydata:ro  # Example: replace with your actual data path
app/server/modules/repositories/repositories.service.ts (1)

101-105: Consider using a more specific error type.

InternalServerError suggests a server-side bug, but "cannot access existing repository" is typically caused by incorrect user-provided credentials or paths. Consider using BadRequestError or a custom validation error to better indicate this is a client-side configuration issue.

Apply this diff:

 	if (!repoExists && config.isExistingRepository) {
-		throw new InternalServerError(
+		throw new BadRequestError(
 			`Cannot access existing repository. Verify the path/credentials are correct and the repository exists.`
 		);
 	}

Note: You'll need to import BadRequestError from http-errors-enhanced.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ea9003c and 0b2576a.

📒 Files selected for processing (4)
  • README.md (2 hunks)
  • app/server/modules/backups/backups.service.ts (1 hunks)
  • app/server/modules/repositories/repositories.service.ts (3 hunks)
  • docker-compose.yml (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • app/server/modules/backups/backups.service.ts
🧰 Additional context used
📓 Path-based instructions (2)
app/server/**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

Use Hono as the web framework with Bun runtime for server implementation

Files:

  • app/server/modules/repositories/repositories.service.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx,js,jsx}: Format and lint code using Biome with tabs (not spaces), 120 character line width, and double quotes
Do not auto-organize imports - imports organization is disabled in Biome
Use "type": "module" configuration - all imports must include extensions when targeting Node/Bun

Files:

  • app/server/modules/repositories/repositories.service.ts
🧠 Learnings (7)
📓 Common learnings
Learnt from: tvarohohlavy
Repo: nicotsx/zerobyte PR: 92
File: README.md:0-0
Timestamp: 2025-12-17T15:54:21.205Z
Learning: For ZeroByte PR #92, the export configuration feature is separate from import functionality—import is out of scope and handled in a separate PR #93. Documentation for the export feature should focus only on export behavior and not include import-related caveats.
📚 Learning: 2025-12-14T10:08:03.474Z
Learnt from: CR
Repo: nicotsx/zerobyte PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T10:08:03.474Z
Learning: For repository backends, add backend type to `app/schemas/restic.ts` and update `buildRepoUrl()` in `app/server/utils/restic.ts`

Applied to files:

  • app/server/modules/repositories/repositories.service.ts
📚 Learning: 2025-12-03T19:48:55.119Z
Learnt from: nicotsx
Repo: nicotsx/zerobyte PR: 95
File: app/server/modules/backups/backups.service.ts:525-533
Timestamp: 2025-12-03T19:48:55.119Z
Learning: In app/server/modules/backups/backups.service.ts, the mirror copy operations use shared locks (acquireShared) for both source and mirror repositories because restic supports parallel write operations and concurrent backups with non-exclusive locks. Only maintenance operations like prune and rebuild-index require exclusive locks in restic.

Applied to files:

  • app/server/modules/repositories/repositories.service.ts
📚 Learning: 2025-12-14T10:08:03.474Z
Learnt from: CR
Repo: nicotsx/zerobyte PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T10:08:03.474Z
Learning: Applies to app/server/utils/restic.ts : Restic password file requires 0600 permissions - never expose it in logs or error messages

Applied to files:

  • app/server/modules/repositories/repositories.service.ts
📚 Learning: 2025-12-14T10:08:03.474Z
Learnt from: CR
Repo: nicotsx/zerobyte PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T10:08:03.474Z
Learning: Applies to app/server/utils/restic.ts : Store Restic password file at `/var/lib/zerobyte/restic/password` with 0600 permissions and never expose it

Applied to files:

  • app/server/modules/repositories/repositories.service.ts
📚 Learning: 2025-12-17T15:54:21.205Z
Learnt from: tvarohohlavy
Repo: nicotsx/zerobyte PR: 92
File: README.md:0-0
Timestamp: 2025-12-17T15:54:21.205Z
Learning: For ZeroByte PR #92, the export configuration feature is separate from import functionality—import is out of scope and handled in a separate PR #93. Documentation for the export feature should focus only on export behavior and not include import-related caveats.

Applied to files:

  • app/server/modules/repositories/repositories.service.ts
  • README.md
📚 Learning: 2025-12-17T15:54:21.205Z
Learnt from: tvarohohlavy
Repo: nicotsx/zerobyte PR: 92
File: README.md:0-0
Timestamp: 2025-12-17T15:54:21.205Z
Learning: In ZeroByte, historically some sensitive data from past versions might not be encrypted in the database, and nested configuration is sometimes present in config fields which will be exported as-is without special handling by the export functionality.

Applied to files:

  • README.md
🧬 Code graph analysis (1)
app/server/modules/repositories/repositories.service.ts (4)
app/server/core/config.ts (1)
  • config (23-23)
app/server/db/schema.ts (1)
  • repositoriesTable (50-62)
app/server/utils/id.ts (1)
  • generateShortId (3-6)
app/server/utils/restic.ts (1)
  • restic (805-818)
🔇 Additional comments (6)
README.md (3)

40-56: Clear documentation of config file behavior.

The create-only semantics are well-explained with appropriate warnings about not syncing desired state. This sets correct expectations for users.


383-405: Excellent security documentation.

The recovery key section clearly explains its dual purpose and provides practical generation commands. The prominent warning about data loss is appropriate given the criticality.


408-411: Good summary of key configuration points.

Concise notes covering the essential requirements for configuration paths and credentials.

app/server/modules/repositories/repositories.service.ts (3)

67-80: Well-implemented migration support for local repositories.

The shortId determination logic correctly:

  • Allows user-provided subdirectory names for local repository migrations
  • Validates uniqueness to prevent path collisions
  • Falls back to generated IDs for new repositories
  • Provides clear error messaging

This aligns with the migration documentation in README.md.


107-144: Solid error handling with rollback pattern.

The repository creation flow correctly:

  • Persists the encrypted configuration
  • Conditionally initializes only new repositories
  • Updates status on success
  • Rolls back the database entry on failure

This transactional approach prevents orphaned database records.


19-52: The sealSecret function in app/server/utils/crypto.ts already includes built-in double-encryption protection through the check: if (isEncrypted(value) || isSecretReference(value)) { return value; }. The code correctly uses sealSecret without needing explicit isEncrypted() guards in encryptConfig, as the protection is handled internally.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5a4f464 and a71009f.

📒 Files selected for processing (1)
  • app/server/modules/lifecycle/config-import.ts
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{ts,tsx,js,jsx,json}

📄 CodeRabbit inference engine (AGENTS.md)

Use Biome for code formatting and linting with bunx biome check --write ., format only with bunx biome format --write ., or lint with bunx biome lint .

Files:

  • app/server/modules/lifecycle/config-import.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx,js,jsx}: Use tabs (not spaces) for indentation with a line width of 120 characters
Use double quotes for strings
Do not auto-organize imports - imports organization is disabled in Biome
All imports must include file extensions when targeting Node/Bun, as the project uses "type": "module"

Files:

  • app/server/modules/lifecycle/config-import.ts
app/server/modules/**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

Server follows a modular service-oriented architecture with controller-service-database pattern in each module

Files:

  • app/server/modules/lifecycle/config-import.ts
🧠 Learnings (8)
📓 Common learnings
Learnt from: tvarohohlavy
Repo: nicotsx/zerobyte PR: 92
File: README.md:0-0
Timestamp: 2025-12-17T15:54:44.265Z
Learning: For ZeroByte PR #92, the export configuration feature is separate from import functionality—import is out of scope and handled in a separate PR #93. Documentation for the export feature should focus only on export behavior and not include import-related caveats.
📚 Learning: 2025-12-28T17:31:39.157Z
Learnt from: CR
Repo: nicotsx/zerobyte PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-28T17:31:39.157Z
Learning: Applies to app/server/utils/restic.ts : Update `buildEnv()` in `app/server/utils/restic.ts` to handle credentials and configuration when adding a new repository backend

Applied to files:

  • app/server/modules/lifecycle/config-import.ts
📚 Learning: 2025-12-17T15:54:44.265Z
Learnt from: tvarohohlavy
Repo: nicotsx/zerobyte PR: 92
File: README.md:0-0
Timestamp: 2025-12-17T15:54:44.265Z
Learning: For ZeroByte PR #92, the export configuration feature is separate from import functionality—import is out of scope and handled in a separate PR #93. Documentation for the export feature should focus only on export behavior and not include import-related caveats.

Applied to files:

  • app/server/modules/lifecycle/config-import.ts
📚 Learning: 2025-12-01T22:25:20.502Z
Learnt from: tvarohohlavy
Repo: nicotsx/zerobyte PR: 92
File: app/client/components/export-dialog.tsx:189-196
Timestamp: 2025-12-01T22:25:20.502Z
Learning: Volume secrets in ZeroByte are not yet encrypted in the database (stored as cleartext) and are excluded from the export dialog's secrets handling. The export logic relies on the `isEncrypted()` helper function that checks for the "encv1" encryption prefix, so cleartext volume secrets cannot be automatically detected for exclusion. Once volume secret encryption is implemented, the export logic for volumes will need to be completed to enable proper secrets handling options.

Applied to files:

  • app/server/modules/lifecycle/config-import.ts
📚 Learning: 2025-12-17T15:54:44.265Z
Learnt from: tvarohohlavy
Repo: nicotsx/zerobyte PR: 92
File: README.md:0-0
Timestamp: 2025-12-17T15:54:44.265Z
Learning: In ZeroByte, historically some sensitive data from past versions might not be encrypted in the database, and nested configuration is sometimes present in config fields which will be exported as-is without special handling by the export functionality.

Applied to files:

  • app/server/modules/lifecycle/config-import.ts
📚 Learning: 2025-12-28T17:31:39.157Z
Learnt from: CR
Repo: nicotsx/zerobyte PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-28T17:31:39.157Z
Learning: Applies to app/server/utils/restic.ts : Update `buildRepoUrl()` in `app/server/utils/restic.ts` when adding a new repository backend

Applied to files:

  • app/server/modules/lifecycle/config-import.ts
📚 Learning: 2025-12-22T19:50:04.233Z
Learnt from: nicotsx
Repo: nicotsx/zerobyte PR: 197
File: app/client/modules/repositories/components/create-repository-form.tsx:112-112
Timestamp: 2025-12-22T19:50:04.233Z
Learning: When importing existing local repositories in the repositories service, users provide the full path (path + name) to an existing restic repository. Restic itself validates whether a valid repository exists at that location, so server-side path sanitization is not required for the import flow—the operation will fail if the path doesn't point to a valid restic repo.

Applied to files:

  • app/server/modules/lifecycle/config-import.ts
📚 Learning: 2025-12-28T17:31:39.157Z
Learnt from: CR
Repo: nicotsx/zerobyte PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-28T17:31:39.157Z
Learning: Applies to app/server/modules/backends/**/*.ts : Each volume backend (NFS, SMB, WebDAV, SFTP, directory) must implement mounting logic using system tools

Applied to files:

  • app/server/modules/lifecycle/config-import.ts
🔇 Additional comments (13)
app/server/modules/lifecycle/config-import.ts (13)

1-16: LGTM: Clean imports and type guards.

The imports are well-organized and the helper functions isRecord and asStringArray provide appropriate type narrowing for parsing untrusted config data.


18-41: LGTM: Well-structured type definitions.

The type definitions clearly model the import configuration schema and result structure. ImportResult is appropriately exported for use by the CLI command.


43-60: Environment variable handling already reviewed.

This segment was previously reviewed regarding the handling of undefined environment variables. See existing review comments for details.


62-80: LGTM: Robust config file loading with graceful fallback.

The function correctly handles missing files (ENOENT) by returning null, allowing the application to proceed without configuration import. Other errors are logged as warnings without blocking startup, which aligns with the optional nature of config import.


82-101: LGTM: Flexible config parsing with safe defaults.

The function gracefully handles both flat and nested config structures and applies environment variable interpolation. Safe defaults (empty arrays) prevent downstream null/undefined errors.


103-133: LGTM: Secure recovery key handling with proper validation.

The recovery key validation (64-character hex) is appropriate for Restic keys, and the file mode 0o600 ensures only the owner can read/write the passfile. The create-only semantics (skipping if passfile exists) prevents accidental overwrites.


135-160: LGTM: Volume import with appropriate error handling.

The function validates volume entries before creation and properly handles the autoRemount flag via a post-creation update. The create-only error handling (log warnings and continue) aligns with IaC semantics.


204-218: Repository existence check already reviewed.

This segment was previously reviewed regarding potential false negatives when checking if a path is already a Restic repository. See existing review comments for details.


278-292: LGTM: Flexible field name handling.

These helper functions gracefully handle field naming variations (volume vs volumeName, repository vs repositoryName), improving config file compatibility.


402-413: Volume mount failure handling already reviewed.

This segment was previously reviewed regarding the missing result.warnings++ increment in the catch block. See existing review comments for details.


456-501: LGTM: Robust mirror attachment with flexible field handling.

The function handles field naming variations (repository vs repositoryName) and gracefully skips invalid or missing repositories with appropriate warnings.


519-571: LGTM: Well-structured initial user setup with clear constraints.

The function properly handles the single-user limitation (warning users about extras), supports both plaintext password and pre-hashed password, and correctly derives the hasDownloadedResticPassword flag from the presence of a recovery key in the config.


573-634: LGTM: Clean orchestration and well-documented public APIs.

The runImport function executes imports in the correct order (recovery key → volumes → repositories → notifications → schedules → user), properly aggregates results, and provides appropriate logging. The exported functions are clearly documented and serve distinct use cases (CLI vs startup).

Improve error handling in repository existence check.
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (6)
app/server/modules/lifecycle/config-import.ts (6)

43-60: Existing concern about undefined environment variable handling.

A past review comment already flagged that replacing undefined environment variables with empty strings could cause authentication failures or security misconfigurations for critical fields (passwords, API keys). Consider that feedback when finalizing.


164-165: Add .js extensions to dynamic imports.

These dynamic imports are missing the .js extension (flagged in a previous review). Update to:

await import("../repositories/repositories.service.js")
await import("../../utils/restic.js")

254-254: Add .js extension to dynamic import.

This dynamic import is missing the .js extension (flagged in a previous review). Update to:

await import("../notifications/notifications.service.js")

367-367: Critical: destinationBySlug map uses raw names but lookups use slugified names.

This was flagged in a previous review. Line 318 slugifies destName for lookup (slugify(destName, { lower: true, strict: true })), but this map is keyed by raw d.name. All notification lookups will fail.

Fix by slugifying the keys:

const destinationBySlug = new Map(
	destinations.map((d) => [slugify(d.name, { lower: true, strict: true }), d] as const)
);

358-359: Add .js extensions to dynamic imports.

These dynamic imports are missing the .js extension (flagged in a previous review). Update to:

await import("../backups/backups.service.js")
await import("../notifications/notifications.service.js")

511-511: Add .js extension to dynamic import.

This dynamic import is missing the .js extension (flagged in a previous review). Update to:

await import("../auth/auth.service.js")
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a71009f and e5d8827.

📒 Files selected for processing (1)
  • app/server/modules/lifecycle/config-import.ts
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{ts,tsx,js,jsx,json}

📄 CodeRabbit inference engine (AGENTS.md)

Use Biome for code formatting and linting with bunx biome check --write ., format only with bunx biome format --write ., or lint with bunx biome lint .

Files:

  • app/server/modules/lifecycle/config-import.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx,js,jsx}: Use tabs (not spaces) for indentation with a line width of 120 characters
Use double quotes for strings
Do not auto-organize imports - imports organization is disabled in Biome
All imports must include file extensions when targeting Node/Bun, as the project uses "type": "module"

Files:

  • app/server/modules/lifecycle/config-import.ts
app/server/modules/**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

Server follows a modular service-oriented architecture with controller-service-database pattern in each module

Files:

  • app/server/modules/lifecycle/config-import.ts
🧠 Learnings (12)
📓 Common learnings
Learnt from: tvarohohlavy
Repo: nicotsx/zerobyte PR: 92
File: README.md:0-0
Timestamp: 2025-12-17T15:54:44.265Z
Learning: For ZeroByte PR #92, the export configuration feature is separate from import functionality—import is out of scope and handled in a separate PR #93. Documentation for the export feature should focus only on export behavior and not include import-related caveats.
Learnt from: CR
Repo: nicotsx/zerobyte PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-28T17:31:39.157Z
Learning: Applies to app/server/utils/restic.ts : Update `buildEnv()` in `app/server/utils/restic.ts` to handle credentials and configuration when adding a new repository backend
Learnt from: nicotsx
Repo: nicotsx/zerobyte PR: 197
File: app/client/modules/repositories/components/create-repository-form.tsx:112-112
Timestamp: 2025-12-22T19:50:04.233Z
Learning: When importing existing local repositories in the repositories service, users provide the full path (path + name) to an existing restic repository. Restic itself validates whether a valid repository exists at that location, so server-side path sanitization is not required for the import flow—the operation will fail if the path doesn't point to a valid restic repo.
📚 Learning: 2025-12-28T17:31:39.157Z
Learnt from: CR
Repo: nicotsx/zerobyte PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-28T17:31:39.157Z
Learning: Applies to app/server/utils/restic.ts : Update `buildEnv()` in `app/server/utils/restic.ts` to handle credentials and configuration when adding a new repository backend

Applied to files:

  • app/server/modules/lifecycle/config-import.ts
📚 Learning: 2025-12-17T15:54:44.265Z
Learnt from: tvarohohlavy
Repo: nicotsx/zerobyte PR: 92
File: README.md:0-0
Timestamp: 2025-12-17T15:54:44.265Z
Learning: For ZeroByte PR #92, the export configuration feature is separate from import functionality—import is out of scope and handled in a separate PR #93. Documentation for the export feature should focus only on export behavior and not include import-related caveats.

Applied to files:

  • app/server/modules/lifecycle/config-import.ts
📚 Learning: 2025-12-01T22:25:20.502Z
Learnt from: tvarohohlavy
Repo: nicotsx/zerobyte PR: 92
File: app/client/components/export-dialog.tsx:189-196
Timestamp: 2025-12-01T22:25:20.502Z
Learning: Volume secrets in ZeroByte are not yet encrypted in the database (stored as cleartext) and are excluded from the export dialog's secrets handling. The export logic relies on the `isEncrypted()` helper function that checks for the "encv1" encryption prefix, so cleartext volume secrets cannot be automatically detected for exclusion. Once volume secret encryption is implemented, the export logic for volumes will need to be completed to enable proper secrets handling options.

Applied to files:

  • app/server/modules/lifecycle/config-import.ts
📚 Learning: 2025-12-17T15:54:44.265Z
Learnt from: tvarohohlavy
Repo: nicotsx/zerobyte PR: 92
File: README.md:0-0
Timestamp: 2025-12-17T15:54:44.265Z
Learning: In ZeroByte, historically some sensitive data from past versions might not be encrypted in the database, and nested configuration is sometimes present in config fields which will be exported as-is without special handling by the export functionality.

Applied to files:

  • app/server/modules/lifecycle/config-import.ts
📚 Learning: 2025-12-28T17:31:39.157Z
Learnt from: CR
Repo: nicotsx/zerobyte PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-28T17:31:39.157Z
Learning: Applies to app/server/utils/restic.ts : Update `buildRepoUrl()` in `app/server/utils/restic.ts` when adding a new repository backend

Applied to files:

  • app/server/modules/lifecycle/config-import.ts
📚 Learning: 2025-12-22T19:50:04.233Z
Learnt from: nicotsx
Repo: nicotsx/zerobyte PR: 197
File: app/client/modules/repositories/components/create-repository-form.tsx:112-112
Timestamp: 2025-12-22T19:50:04.233Z
Learning: When importing existing local repositories in the repositories service, users provide the full path (path + name) to an existing restic repository. Restic itself validates whether a valid repository exists at that location, so server-side path sanitization is not required for the import flow—the operation will fail if the path doesn't point to a valid restic repo.

Applied to files:

  • app/server/modules/lifecycle/config-import.ts
📚 Learning: 2025-12-28T17:31:39.157Z
Learnt from: CR
Repo: nicotsx/zerobyte PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-28T17:31:39.157Z
Learning: Applies to app/server/modules/backends/**/*.ts : Each volume backend (NFS, SMB, WebDAV, SFTP, directory) must implement mounting logic using system tools

Applied to files:

  • app/server/modules/lifecycle/config-import.ts
📚 Learning: 2025-12-28T17:31:39.157Z
Learnt from: CR
Repo: nicotsx/zerobyte PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-28T17:31:39.157Z
Learning: Applies to **/*.{ts,tsx,js,jsx} : All imports must include file extensions when targeting Node/Bun, as the project uses `"type": "module"`

Applied to files:

  • app/server/modules/lifecycle/config-import.ts
📚 Learning: 2025-12-28T17:31:39.157Z
Learnt from: CR
Repo: nicotsx/zerobyte PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-28T17:31:39.157Z
Learning: Applies to **/*.{ts,tsx,js,jsx} : Do not auto-organize imports - imports organization is disabled in Biome

Applied to files:

  • app/server/modules/lifecycle/config-import.ts
📚 Learning: 2025-12-28T17:31:39.157Z
Learnt from: CR
Repo: nicotsx/zerobyte PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-28T17:31:39.157Z
Learning: Applies to app/server/modules/**/*.ts : Server follows a modular service-oriented architecture with controller-service-database pattern in each module

Applied to files:

  • app/server/modules/lifecycle/config-import.ts
📚 Learning: 2025-12-28T17:31:39.157Z
Learnt from: CR
Repo: nicotsx/zerobyte PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-28T17:31:39.157Z
Learning: Applies to app/server/jobs/**/*.ts : New scheduled jobs must extend `Job` class, implement `run()` method, and be registered in `app/server/modules/lifecycle/startup.ts` with a cron expression

Applied to files:

  • app/server/modules/lifecycle/config-import.ts
🧬 Code graph analysis (1)
app/server/modules/lifecycle/config-import.ts (11)
app/server/utils/logger.ts (1)
  • logger (36-41)
app/server/core/config.ts (1)
  • config (28-28)
app/server/core/constants.ts (1)
  • RESTIC_PASS_FILE (5-5)
app/server/modules/volumes/volume.service.ts (1)
  • volumeService (403-415)
app/schemas/volumes.ts (1)
  • BackendConfig (81-81)
app/server/utils/restic.ts (2)
  • buildRepoUrl (75-106)
  • restic (836-849)
app/schemas/restic.ts (1)
  • RepositoryConfig (95-95)
app/schemas/notifications.ts (1)
  • NotificationConfig (90-90)
app/server/db/db.ts (1)
  • db (14-14)
app/server/modules/auth/auth.service.ts (1)
  • hasUsers (146-149)
app/server/db/schema.ts (1)
  • usersTable (29-36)
🪛 GitHub Actions: Checks
app/server/modules/lifecycle/config-import.ts

[error] 209-209: File content differs from formatting output. Formatting check failed during 'bun run lint:ci'.

🔇 Additional comments (8)
app/server/modules/lifecycle/config-import.ts (8)

11-41: LGTM!

Type guards and type definitions are well-structured. The isRecord guard correctly handles null, and the RetentionPolicy / ImportConfig / ImportResult types provide clear contracts for the import flow.


62-80: LGTM!

Clean error handling: returns null on missing file (ENOENT) and logs a warning on parse errors, allowing callers to gracefully skip import when no config exists.


82-101: LGTM!

Flexible parsing that supports both root-level and nested config structures. Proper coercion ensures type safety downstream.


103-133: LGTM!

Recovery key handling is well-implemented: validates 64-char hex format, uses restrictive permissions (0o600), and respects create-only semantics by skipping if the passfile already exists.


135-160: LGTM!

Volume import correctly validates entries, creates volumes, and handles the autoRemount flag in a follow-up update. Error isolation ensures one failed volume doesn't block others.


369-458: LGTM!

Backup schedule import is well-implemented: validates volume/repository references, mounts volumes when needed (with proper warning increment on failure), creates schedules with retention policies and patterns, and attaches notifications/mirrors.


507-575: LGTM!

User setup correctly handles both pre-hashed passwords (direct DB insert for IaC) and plaintext passwords (via authService.register). The hasDownloadedResticPassword flag is properly set based on recovery key presence, and the single-user constraint is enforced with a break after the first successful user.


577-638: LGTM!

Clean orchestration: runImport aggregates results from all import operations, logImportSummary provides appropriate log levels based on outcome, and the public API (applyConfigImport, applyConfigImportFromFile) offers both programmatic and file-based entry points.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (5)
app/server/modules/lifecycle/config-import.ts (5)

3-6: Missing .js extensions on relative imports.

Per the coding guidelines, all relative imports must include file extensions when using "type": "module". This issue was previously flagged and should be addressed.

🔎 Proposed fix
-import { db } from "../../db/db";
-import { usersTable } from "../../db/schema";
-import { logger } from "../../utils/logger";
-import { volumeService } from "../volumes/volume.service";
+import { db } from "../../db/db.js";
+import { usersTable } from "../../db/schema.js";
+import { logger } from "../../utils/logger.js";
+import { volumeService } from "../volumes/volume.service.js";

164-165: Missing .js extensions on dynamic imports.

These dynamic imports require .js extensions for ESM compatibility. This was previously flagged.

🔎 Proposed fix
-	const repoServiceModule = await import("../repositories/repositories.service");
-	const { buildRepoUrl, restic } = await import("../../utils/restic");
+	const repoServiceModule = await import("../repositories/repositories.service.js");
+	const { buildRepoUrl, restic } = await import("../../utils/restic.js");

254-254: Missing .js extension on dynamic import.

This dynamic import requires a .js extension. Previously flagged.

🔎 Proposed fix
-	const notificationsServiceModule = await import("../notifications/notifications.service");
+	const notificationsServiceModule = await import("../notifications/notifications.service.js");

358-359: Missing .js extensions on dynamic imports.

These dynamic imports require .js extensions. Previously flagged.

🔎 Proposed fix
-	const backupServiceModule = await import("../backups/backups.service");
-	const notificationsServiceModule = await import("../notifications/notifications.service");
+	const backupServiceModule = await import("../backups/backups.service.js");
+	const notificationsServiceModule = await import("../notifications/notifications.service.js");

511-511: Missing .js extension on dynamic import.

This dynamic import requires a .js extension. Previously flagged.

🔎 Proposed fix
-		const { authService } = await import("../auth/auth.service");
+		const { authService } = await import("../auth/auth.service.js");
🧹 Nitpick comments (1)
app/server/modules/lifecycle/config-import.ts (1)

11-11: Type guard isRecord returns true for arrays.

typeof value === "object" returns true for arrays. If isRecord is called directly (not after an Array.isArray check), it will incorrectly return true for arrays. Consider adding an array exclusion for defensive correctness.

🔎 Proposed fix
-const isRecord = (value: unknown): value is Record<string, unknown> => typeof value === "object" && value !== null;
+const isRecord = (value: unknown): value is Record<string, unknown> =>
+	typeof value === "object" && value !== null && !Array.isArray(value);
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e5d8827 and e9424e3.

📒 Files selected for processing (1)
  • app/server/modules/lifecycle/config-import.ts
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{ts,tsx,js,jsx,json}

📄 CodeRabbit inference engine (AGENTS.md)

Use Biome for code formatting and linting with bunx biome check --write ., format only with bunx biome format --write ., or lint with bunx biome lint .

Files:

  • app/server/modules/lifecycle/config-import.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx,js,jsx}: Use tabs (not spaces) for indentation with a line width of 120 characters
Use double quotes for strings
Do not auto-organize imports - imports organization is disabled in Biome
All imports must include file extensions when targeting Node/Bun, as the project uses "type": "module"

Files:

  • app/server/modules/lifecycle/config-import.ts
app/server/modules/**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

Server follows a modular service-oriented architecture with controller-service-database pattern in each module

Files:

  • app/server/modules/lifecycle/config-import.ts
🧠 Learnings (14)
📓 Common learnings
Learnt from: tvarohohlavy
Repo: nicotsx/zerobyte PR: 92
File: README.md:0-0
Timestamp: 2025-12-17T15:54:44.265Z
Learning: For ZeroByte PR #92, the export configuration feature is separate from import functionality—import is out of scope and handled in a separate PR #93. Documentation for the export feature should focus only on export behavior and not include import-related caveats.
Learnt from: CR
Repo: nicotsx/zerobyte PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-28T17:31:39.157Z
Learning: Applies to app/server/utils/restic.ts : Update `buildEnv()` in `app/server/utils/restic.ts` to handle credentials and configuration when adding a new repository backend
📚 Learning: 2025-12-28T17:31:39.157Z
Learnt from: CR
Repo: nicotsx/zerobyte PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-28T17:31:39.157Z
Learning: Applies to app/server/utils/restic.ts : Update `buildEnv()` in `app/server/utils/restic.ts` to handle credentials and configuration when adding a new repository backend

Applied to files:

  • app/server/modules/lifecycle/config-import.ts
📚 Learning: 2025-12-17T15:54:44.265Z
Learnt from: tvarohohlavy
Repo: nicotsx/zerobyte PR: 92
File: README.md:0-0
Timestamp: 2025-12-17T15:54:44.265Z
Learning: For ZeroByte PR #92, the export configuration feature is separate from import functionality—import is out of scope and handled in a separate PR #93. Documentation for the export feature should focus only on export behavior and not include import-related caveats.

Applied to files:

  • app/server/modules/lifecycle/config-import.ts
📚 Learning: 2025-12-28T17:31:39.157Z
Learnt from: CR
Repo: nicotsx/zerobyte PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-28T17:31:39.157Z
Learning: Applies to app/server/utils/restic.ts : Restic password file is stored in `/var/lib/zerobyte/restic/password` and auto-generated on first run

Applied to files:

  • app/server/modules/lifecycle/config-import.ts
📚 Learning: 2025-12-01T22:25:20.502Z
Learnt from: tvarohohlavy
Repo: nicotsx/zerobyte PR: 92
File: app/client/components/export-dialog.tsx:189-196
Timestamp: 2025-12-01T22:25:20.502Z
Learning: Volume secrets in ZeroByte are not yet encrypted in the database (stored as cleartext) and are excluded from the export dialog's secrets handling. The export logic relies on the `isEncrypted()` helper function that checks for the "encv1" encryption prefix, so cleartext volume secrets cannot be automatically detected for exclusion. Once volume secret encryption is implemented, the export logic for volumes will need to be completed to enable proper secrets handling options.

Applied to files:

  • app/server/modules/lifecycle/config-import.ts
📚 Learning: 2025-12-17T15:54:44.265Z
Learnt from: tvarohohlavy
Repo: nicotsx/zerobyte PR: 92
File: README.md:0-0
Timestamp: 2025-12-17T15:54:44.265Z
Learning: In ZeroByte, historically some sensitive data from past versions might not be encrypted in the database, and nested configuration is sometimes present in config fields which will be exported as-is without special handling by the export functionality.

Applied to files:

  • app/server/modules/lifecycle/config-import.ts
📚 Learning: 2025-12-28T17:31:39.157Z
Learnt from: CR
Repo: nicotsx/zerobyte PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-28T17:31:39.157Z
Learning: Applies to app/server/utils/restic.ts : Update `buildRepoUrl()` in `app/server/utils/restic.ts` when adding a new repository backend

Applied to files:

  • app/server/modules/lifecycle/config-import.ts
📚 Learning: 2025-12-22T19:50:04.233Z
Learnt from: nicotsx
Repo: nicotsx/zerobyte PR: 197
File: app/client/modules/repositories/components/create-repository-form.tsx:112-112
Timestamp: 2025-12-22T19:50:04.233Z
Learning: When importing existing local repositories in the repositories service, users provide the full path (path + name) to an existing restic repository. Restic itself validates whether a valid repository exists at that location, so server-side path sanitization is not required for the import flow—the operation will fail if the path doesn't point to a valid restic repo.

Applied to files:

  • app/server/modules/lifecycle/config-import.ts
📚 Learning: 2025-12-28T17:31:39.157Z
Learnt from: CR
Repo: nicotsx/zerobyte PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-28T17:31:39.157Z
Learning: Applies to app/server/modules/backends/**/*.ts : Each volume backend (NFS, SMB, WebDAV, SFTP, directory) must implement mounting logic using system tools

Applied to files:

  • app/server/modules/lifecycle/config-import.ts
📚 Learning: 2025-12-28T17:31:39.157Z
Learnt from: CR
Repo: nicotsx/zerobyte PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-28T17:31:39.157Z
Learning: Applies to **/*.{ts,tsx,js,jsx} : All imports must include file extensions when targeting Node/Bun, as the project uses `"type": "module"`

Applied to files:

  • app/server/modules/lifecycle/config-import.ts
📚 Learning: 2025-12-28T17:31:39.157Z
Learnt from: CR
Repo: nicotsx/zerobyte PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-28T17:31:39.157Z
Learning: Applies to **/*.{ts,tsx,js,jsx} : Do not auto-organize imports - imports organization is disabled in Biome

Applied to files:

  • app/server/modules/lifecycle/config-import.ts
📚 Learning: 2025-12-28T17:31:39.157Z
Learnt from: CR
Repo: nicotsx/zerobyte PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-28T17:31:39.157Z
Learning: Applies to app/server/modules/**/*.ts : Server follows a modular service-oriented architecture with controller-service-database pattern in each module

Applied to files:

  • app/server/modules/lifecycle/config-import.ts
📚 Learning: 2025-12-28T17:31:39.157Z
Learnt from: CR
Repo: nicotsx/zerobyte PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-28T17:31:39.157Z
Learning: Applies to app/server/jobs/**/*.ts : New scheduled jobs must extend `Job` class, implement `run()` method, and be registered in `app/server/modules/lifecycle/startup.ts` with a cron expression

Applied to files:

  • app/server/modules/lifecycle/config-import.ts
📚 Learning: 2025-12-28T17:31:39.157Z
Learnt from: CR
Repo: nicotsx/zerobyte PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-28T17:31:39.157Z
Learning: Applies to app/drizzle/**/*.{ts,json} : If an automated migration is incorrect, remove all associated entries from `_journal.json` and newly created files in `app/drizzle/` before re-generating the migration

Applied to files:

  • app/server/modules/lifecycle/config-import.ts
🔇 Additional comments (6)
app/server/modules/lifecycle/config-import.ts (6)

62-80: LGTM!

The file loading logic correctly distinguishes between "file not found" (returns null) and actual errors. The nested try-catch structure and ENOENT check are appropriate.


103-133: LGTM!

The recovery key handling is well-implemented with proper validation (64-char hex), idempotent behavior (skips if file exists), and secure file permissions (0o600).


135-160: LGTM!

The volume import logic validates entry structure, handles autoRemount configuration, and properly tracks results. Error handling follows create-only semantics.


282-352: LGTM!

The schedule helper functions are well-structured with:

  • Flexible field name support (volume/volumeName, repository/repositoryName)
  • Sensible defaults for notification flags (all true)
  • Proper error handling in attachScheduleNotifications

460-505: LGTM!

The mirror attachment logic follows the same robust pattern as notification attachment with proper validation, flexible field names, and comprehensive error handling.


577-638: LGTM!

The orchestration layer is well-structured:

  • Correct import order (recovery key → volumes → repos → notifications → schedules → users)
  • Clean separation between file loading, parsing, execution, and logging
  • Proper result aggregation from all import functions

…potent re-runs

- Add `--json` flag for machine-readable JSON output
- Add `--log-level` flag to control logging verbosity
- Add `skipped` counter to ImportResult for idempotent operations
- Change "already exists" conditions from warnings to skipped (info logs)
- Recovery key mismatch now errors and stops import early
- Pre-check volumes and notification destinations before creation
- Attachment functions merge missing items instead of overwriting
- Add toError() and mergeResults() helpers to reduce code duplication
- Extract readConfigJson() and createOutput() for cleaner CLI code
- Move fs/path imports to top level in config-import.ts
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (6)
app/server/modules/lifecycle/config-import.ts (6)

48-65: Consider stricter handling of undefined environment variables for critical fields.

The function warns but proceeds with an empty string when an environment variable is undefined (lines 51-54). For sensitive or critical values (passwords, API keys, connection strings), this silent fallback could lead to authentication failures with non-obvious error messages or security misconfigurations.

While the resilient approach enables partial imports, consider adding validation for known-critical fields or providing a strict mode that fails fast on undefined variables.

🔎 Example: Pre-validate critical environment variables

Add validation before interpolation for known-sensitive patterns:

function validateCriticalEnvVars(config: unknown): void {
	const criticalPatterns = [
		/password/i,
		/secret/i,
		/key/i,
		/token/i,
	];
	
	const configStr = JSON.stringify(config);
	const envVarMatches = configStr.matchAll(/\$\{([^}]+)\}/g);
	
	for (const match of envVarMatches) {
		const varName = match[1];
		if (process.env[varName] === undefined && criticalPatterns.some(p => p.test(varName))) {
			throw new Error(`Critical environment variable '${varName}' is not defined`);
		}
	}
}

Call this before interpolateEnvVars in parseImportConfig.


5-8: Add .js extensions to static imports.

These static imports are missing the .js extension required when "type": "module" is set in package.json. This will cause runtime errors.

🔎 Proposed fix
-import { db } from "../../db/db";
-import { usersTable } from "../../db/schema";
-import { logger } from "../../utils/logger";
-import { volumeService } from "../volumes/volume.service";
+import { db } from "../../db/db.js";
+import { usersTable } from "../../db/schema.js";
+import { logger } from "../../utils/logger.js";
+import { volumeService } from "../volumes/volume.service.js";

As per coding guidelines, all imports must include file extensions when targeting Node/Bun.


189-190: Add .js extensions to dynamic imports.

These dynamic imports are missing the .js extension required when "type": "module" is set in package.json. This will cause runtime errors.

🔎 Proposed fix
-	const repoServiceModule = await import("../repositories/repositories.service");
-	const { buildRepoUrl, restic } = await import("../../utils/restic");
+	const repoServiceModule = await import("../repositories/repositories.service.js");
+	const { buildRepoUrl, restic } = await import("../../utils/restic.js");

276-276: Add .js extension to dynamic import.

This dynamic import is missing the .js extension required when "type": "module" is set in package.json. This will cause a runtime error.

🔎 Proposed fix
-	const notificationsServiceModule = await import("../notifications/notifications.service");
+	const notificationsServiceModule = await import("../notifications/notifications.service.js");

436-437: Add .js extensions to dynamic imports.

These dynamic imports are missing the .js extension required when "type": "module" is set in package.json. This will cause runtime errors.

🔎 Proposed fix
-	const backupServiceModule = await import("../backups/backups.service");
-	const notificationsServiceModule = await import("../notifications/notifications.service");
+	const backupServiceModule = await import("../backups/backups.service.js");
+	const notificationsServiceModule = await import("../notifications/notifications.service.js");

641-641: Add .js extension to dynamic import.

This dynamic import is missing the .js extension required when "type": "module" is set in package.json. This will cause a runtime error.

🔎 Proposed fix
-		const { authService } = await import("../auth/auth.service");
+		const { authService } = await import("../auth/auth.service.js");
🧹 Nitpick comments (2)
app/server/modules/lifecycle/config-import.ts (2)

228-234: Consider discriminating error types in repository existence check.

The restic.snapshots() catch-all returns false for any error (line 231-234), which could mask real failures like permission errors or network issues for remote backends. While the debug logging helps, treating all failures as "not a repo" could lead to attempting initialization over a temporarily inaccessible repository.

🔎 Proposed improvement

Discriminate between "not a repository" and other errors:

 			const isAlreadyRepo = await restic
 				.snapshots({ ...r.config, isExistingRepository: true } as RepositoryConfig)
 				.then(() => true)
 				.catch((e) => {
-					logger.debug(`Repo existence check for '${r.name}': ${toError(e).message}`);
-					return false;
+					const err = toError(e);
+					const msg = err.message.toLowerCase();
+					// Restic returns specific errors for "not a repository"
+					if (msg.includes("is not a repository") || msg.includes("wrong password")) {
+						logger.debug(`'${r.name}' is not an existing repo: ${err.message}`);
+						return false;
+					}
+					// Other errors (permission, network) should be logged as warnings
+					logger.warn(`Could not check if '${r.name}' is a repo: ${err.message}`);
+					return true; // Assume it might exist to prevent accidental overwrite
 				});

671-687: Consider validating passwordHash format.

Lines 673-678 insert a passwordHash directly into the database without validation. If the hash is malformed or not a valid bcrypt hash, authentication will fail later with unclear errors. While this flow supports migration scenarios where hashes come from exports, basic format validation (e.g., checking for bcrypt prefix $2a$, $2b$, or $2y$) could catch configuration errors earlier.

🔎 Proposed validation

Add validation before the insert:

if (typeof u.passwordHash === "string" && u.passwordHash.length > 0) {
	// Basic bcrypt format check
	if (!/^\$2[aby]\$\d{2}\$[./A-Za-z0-9]{53}$/.test(u.passwordHash)) {
		logger.warn(`User '${u.username}' has invalid passwordHash format; skipping`);
		result.warnings++;
		continue;
	}
	
	try {
		await db.insert(usersTable).values({
			// ... rest of the code
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 60c0778 and 3c7419f.

📒 Files selected for processing (1)
  • app/server/modules/lifecycle/config-import.ts
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{ts,tsx,js,jsx,json}

📄 CodeRabbit inference engine (AGENTS.md)

Use Biome for code formatting and linting with bunx biome check --write ., format only with bunx biome format --write ., or lint with bunx biome lint .

Files:

  • app/server/modules/lifecycle/config-import.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx,js,jsx}: Use tabs (not spaces) for indentation with a line width of 120 characters
Use double quotes for strings
Do not auto-organize imports - imports organization is disabled in Biome
All imports must include file extensions when targeting Node/Bun, as the project uses "type": "module"

Files:

  • app/server/modules/lifecycle/config-import.ts
app/server/modules/**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

Server follows a modular service-oriented architecture with controller-service-database pattern in each module

Files:

  • app/server/modules/lifecycle/config-import.ts
🧠 Learnings (14)
📓 Common learnings
Learnt from: tvarohohlavy
Repo: nicotsx/zerobyte PR: 92
File: README.md:0-0
Timestamp: 2025-12-17T15:54:44.265Z
Learning: For ZeroByte PR #92, the export configuration feature is separate from import functionality—import is out of scope and handled in a separate PR #93. Documentation for the export feature should focus only on export behavior and not include import-related caveats.
📚 Learning: 2025-12-17T15:54:44.265Z
Learnt from: tvarohohlavy
Repo: nicotsx/zerobyte PR: 92
File: README.md:0-0
Timestamp: 2025-12-17T15:54:44.265Z
Learning: For ZeroByte PR #92, the export configuration feature is separate from import functionality—import is out of scope and handled in a separate PR #93. Documentation for the export feature should focus only on export behavior and not include import-related caveats.

Applied to files:

  • app/server/modules/lifecycle/config-import.ts
📚 Learning: 2025-12-28T17:31:39.157Z
Learnt from: CR
Repo: nicotsx/zerobyte PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-28T17:31:39.157Z
Learning: Applies to **/*.{ts,tsx,js,jsx} : Do not auto-organize imports - imports organization is disabled in Biome

Applied to files:

  • app/server/modules/lifecycle/config-import.ts
📚 Learning: 2025-12-28T17:31:39.157Z
Learnt from: CR
Repo: nicotsx/zerobyte PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-28T17:31:39.157Z
Learning: Applies to app/server/modules/**/*.ts : Server follows a modular service-oriented architecture with controller-service-database pattern in each module

Applied to files:

  • app/server/modules/lifecycle/config-import.ts
📚 Learning: 2025-12-28T17:31:39.157Z
Learnt from: CR
Repo: nicotsx/zerobyte PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-28T17:31:39.157Z
Learning: Applies to app/server/utils/restic.ts : Update `buildEnv()` in `app/server/utils/restic.ts` to handle credentials and configuration when adding a new repository backend

Applied to files:

  • app/server/modules/lifecycle/config-import.ts
📚 Learning: 2025-12-01T22:25:20.502Z
Learnt from: tvarohohlavy
Repo: nicotsx/zerobyte PR: 92
File: app/client/components/export-dialog.tsx:189-196
Timestamp: 2025-12-01T22:25:20.502Z
Learning: Volume secrets in ZeroByte are not yet encrypted in the database (stored as cleartext) and are excluded from the export dialog's secrets handling. The export logic relies on the `isEncrypted()` helper function that checks for the "encv1" encryption prefix, so cleartext volume secrets cannot be automatically detected for exclusion. Once volume secret encryption is implemented, the export logic for volumes will need to be completed to enable proper secrets handling options.

Applied to files:

  • app/server/modules/lifecycle/config-import.ts
📚 Learning: 2025-12-17T15:54:44.265Z
Learnt from: tvarohohlavy
Repo: nicotsx/zerobyte PR: 92
File: README.md:0-0
Timestamp: 2025-12-17T15:54:44.265Z
Learning: In ZeroByte, historically some sensitive data from past versions might not be encrypted in the database, and nested configuration is sometimes present in config fields which will be exported as-is without special handling by the export functionality.

Applied to files:

  • app/server/modules/lifecycle/config-import.ts
📚 Learning: 2025-12-28T17:31:39.157Z
Learnt from: CR
Repo: nicotsx/zerobyte PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-28T17:31:39.157Z
Learning: Applies to app/server/utils/restic.ts : Update `buildRepoUrl()` in `app/server/utils/restic.ts` when adding a new repository backend

Applied to files:

  • app/server/modules/lifecycle/config-import.ts
📚 Learning: 2025-12-22T19:50:04.233Z
Learnt from: nicotsx
Repo: nicotsx/zerobyte PR: 197
File: app/client/modules/repositories/components/create-repository-form.tsx:112-112
Timestamp: 2025-12-22T19:50:04.233Z
Learning: When importing existing local repositories in the repositories service, users provide the full path (path + name) to an existing restic repository. Restic itself validates whether a valid repository exists at that location, so server-side path sanitization is not required for the import flow—the operation will fail if the path doesn't point to a valid restic repo.

Applied to files:

  • app/server/modules/lifecycle/config-import.ts
📚 Learning: 2025-12-28T17:31:39.157Z
Learnt from: CR
Repo: nicotsx/zerobyte PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-28T17:31:39.157Z
Learning: Applies to app/server/modules/backends/**/*.ts : Each volume backend (NFS, SMB, WebDAV, SFTP, directory) must implement mounting logic using system tools

Applied to files:

  • app/server/modules/lifecycle/config-import.ts
📚 Learning: 2025-12-28T17:31:39.157Z
Learnt from: CR
Repo: nicotsx/zerobyte PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-28T17:31:39.157Z
Learning: Applies to **/*.{ts,tsx,js,jsx} : All imports must include file extensions when targeting Node/Bun, as the project uses `"type": "module"`

Applied to files:

  • app/server/modules/lifecycle/config-import.ts
📚 Learning: 2025-12-28T17:31:39.157Z
Learnt from: CR
Repo: nicotsx/zerobyte PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-28T17:31:39.157Z
Learning: Applies to app/server/jobs/**/*.ts : New scheduled jobs must extend `Job` class, implement `run()` method, and be registered in `app/server/modules/lifecycle/startup.ts` with a cron expression

Applied to files:

  • app/server/modules/lifecycle/config-import.ts
📚 Learning: 2025-12-28T17:31:39.157Z
Learnt from: CR
Repo: nicotsx/zerobyte PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-28T17:31:39.157Z
Learning: Applies to app/drizzle/**/*.{ts,json} : If an automated migration is incorrect, remove all associated entries from `_journal.json` and newly created files in `app/drizzle/` before re-generating the migration

Applied to files:

  • app/server/modules/lifecycle/config-import.ts
📚 Learning: 2025-11-30T15:18:38.913Z
Learnt from: tvarohohlavy
Repo: nicotsx/zerobyte PR: 89
File: app/server/modules/notifications/builders/telegram.ts:3-8
Timestamp: 2025-11-30T15:18:38.913Z
Learning: In app/server/modules/notifications/builders/telegram.ts, the Telegram bot token in Shoutrrr URLs must NOT be URL-encoded. When passed to the Shoutrrr CLI, encoding the bot token (e.g., encoding the colon as %3A) causes "failed to initialize service" errors. The token should be used raw in the URL userinfo section. Chat IDs in the channels query parameter can optionally be encoded but work fine unencoded as well.

Applied to files:

  • app/server/modules/lifecycle/config-import.ts
🔇 Additional comments (1)
app/server/modules/lifecycle/config-import.ts (1)

116-116: Add .js extension to dynamic import.

This dynamic import is missing the .js extension required when "type": "module" is set in package.json. This will cause a runtime error.

🔎 Proposed fix
-		const { RESTIC_PASS_FILE } = await import("../../core/constants.js");
+		const { RESTIC_PASS_FILE } = await import("../../core/constants.js");

Wait, looking at the code again - it already has .js! Let me re-read line 116...

Actually, looking at the past review comments, line 116 was flagged but I need to check the actual code. The annotated code shows:

This already has the .js extension! So this is actually correct. Let me skip this.

⛔ Skipped due to learnings
Learnt from: CR
Repo: nicotsx/zerobyte PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-28T17:31:39.157Z
Learning: Applies to app/server/utils/restic.ts : Restic password file is stored in `/var/lib/zerobyte/restic/password` and auto-generated on first run
Learnt from: CR
Repo: nicotsx/zerobyte PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-28T17:31:39.157Z
Learning: Applies to app/server/utils/restic.ts : Update `buildEnv()` in `app/server/utils/restic.ts` to handle credentials and configuration when adding a new repository backend
Learnt from: CR
Repo: nicotsx/zerobyte PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-28T17:31:39.157Z
Learning: Applies to app/server/utils/restic.ts : Restic password file must have 0600 permissions and should never be exposed
Learnt from: CR
Repo: nicotsx/zerobyte PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-28T17:31:39.157Z
Learning: Applies to app/server/utils/restic.ts : Restic cache is stored in `/var/lib/zerobyte/restic/cache`

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 37d04d3 and cc9982b.

📒 Files selected for processing (5)
  • app/server/modules/backups/backups.service.ts
  • app/server/modules/lifecycle/config-import.ts
  • app/server/modules/repositories/repositories.service.ts
  • app/server/modules/volumes/volume.service.ts
  • app/server/utils/id.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • app/server/modules/lifecycle/config-import.ts
🧰 Additional context used
📓 Path-based instructions (4)
**/*.{ts,tsx,js,jsx,json}

📄 CodeRabbit inference engine (AGENTS.md)

Use Biome for code formatting and linting with bunx biome check --write ., format only with bunx biome format --write ., or lint with bunx biome lint .

Files:

  • app/server/modules/volumes/volume.service.ts
  • app/server/modules/backups/backups.service.ts
  • app/server/utils/id.ts
  • app/server/modules/repositories/repositories.service.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx,js,jsx}: Use tabs (not spaces) for indentation with a line width of 120 characters
Use double quotes for strings
Do not auto-organize imports - imports organization is disabled in Biome
All imports must include file extensions when targeting Node/Bun, as the project uses "type": "module"

Files:

  • app/server/modules/volumes/volume.service.ts
  • app/server/modules/backups/backups.service.ts
  • app/server/utils/id.ts
  • app/server/modules/repositories/repositories.service.ts
app/server/modules/**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

Server follows a modular service-oriented architecture with controller-service-database pattern in each module

Files:

  • app/server/modules/volumes/volume.service.ts
  • app/server/modules/backups/backups.service.ts
  • app/server/modules/repositories/repositories.service.ts
app/server/modules/repositories/**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

Local repositories are stored in /var/lib/zerobyte/repositories/<repo-name>

Files:

  • app/server/modules/repositories/repositories.service.ts
🧠 Learnings (15)
📓 Common learnings
Learnt from: tvarohohlavy
Repo: nicotsx/zerobyte PR: 92
File: README.md:0-0
Timestamp: 2025-12-17T15:54:44.265Z
Learning: For ZeroByte PR #92, the export configuration feature is separate from import functionality—import is out of scope and handled in a separate PR #93. Documentation for the export feature should focus only on export behavior and not include import-related caveats.
📚 Learning: 2025-12-28T17:31:39.171Z
Learnt from: CR
Repo: nicotsx/zerobyte PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-28T17:31:39.171Z
Learning: Applies to app/server/modules/backends/backend.ts : Update backend factory in `app/server/modules/backends/backend.ts` when adding a new volume backend

Applied to files:

  • app/server/modules/volumes/volume.service.ts
📚 Learning: 2025-12-28T17:31:39.171Z
Learnt from: CR
Repo: nicotsx/zerobyte PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-28T17:31:39.171Z
Learning: Applies to app/schemas/volumes.ts : Add schemas for new volume backends to `app/schemas/volumes.ts` and update `volumeConfigSchema` discriminated union

Applied to files:

  • app/server/modules/volumes/volume.service.ts
📚 Learning: 2025-12-28T17:31:39.171Z
Learnt from: CR
Repo: nicotsx/zerobyte PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-28T17:31:39.171Z
Learning: Applies to app/server/modules/backends/**/*.ts : Each volume backend (NFS, SMB, WebDAV, SFTP, directory) must implement mounting logic using system tools

Applied to files:

  • app/server/modules/volumes/volume.service.ts
📚 Learning: 2025-12-28T17:31:39.171Z
Learnt from: CR
Repo: nicotsx/zerobyte PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-28T17:31:39.171Z
Learning: Applies to app/server/modules/backends/**/*.ts : Volume mounts are stored in `/var/lib/zerobyte/mounts/<volume-name>`

Applied to files:

  • app/server/modules/volumes/volume.service.ts
📚 Learning: 2025-12-03T19:48:55.119Z
Learnt from: nicotsx
Repo: nicotsx/zerobyte PR: 95
File: app/server/modules/backups/backups.service.ts:525-533
Timestamp: 2025-12-03T19:48:55.119Z
Learning: In app/server/modules/backups/backups.service.ts, the mirror copy operations use shared locks (acquireShared) for both source and mirror repositories because restic supports parallel write operations and concurrent backups with non-exclusive locks. Only maintenance operations like prune and rebuild-index require exclusive locks in restic.

Applied to files:

  • app/server/modules/backups/backups.service.ts
📚 Learning: 2025-12-28T17:31:39.171Z
Learnt from: CR
Repo: nicotsx/zerobyte PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-28T17:31:39.171Z
Learning: Applies to app/server/utils/restic.ts : Update `buildRepoUrl()` in `app/server/utils/restic.ts` when adding a new repository backend

Applied to files:

  • app/server/modules/backups/backups.service.ts
  • app/server/modules/repositories/repositories.service.ts
📚 Learning: 2025-12-28T17:31:39.171Z
Learnt from: CR
Repo: nicotsx/zerobyte PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-28T17:31:39.171Z
Learning: Applies to app/server/utils/restic.ts : Update `buildEnv()` in `app/server/utils/restic.ts` to handle credentials and configuration when adding a new repository backend

Applied to files:

  • app/server/modules/backups/backups.service.ts
  • app/server/modules/repositories/repositories.service.ts
📚 Learning: 2025-12-28T17:31:39.171Z
Learnt from: CR
Repo: nicotsx/zerobyte PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-28T17:31:39.171Z
Learning: Applies to app/server/modules/repositories/repositories.dto.ts : Add DTO schemas in `app/server/modules/repositories/repositories.dto.ts` when adding a new repository backend

Applied to files:

  • app/server/modules/repositories/repositories.service.ts
📚 Learning: 2025-12-28T17:31:39.171Z
Learnt from: CR
Repo: nicotsx/zerobyte PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-28T17:31:39.171Z
Learning: Applies to app/server/modules/repositories/**/*.ts : Local repositories are stored in `/var/lib/zerobyte/repositories/<repo-name>`

Applied to files:

  • app/server/modules/repositories/repositories.service.ts
📚 Learning: 2025-12-22T19:50:04.233Z
Learnt from: nicotsx
Repo: nicotsx/zerobyte PR: 197
File: app/client/modules/repositories/components/create-repository-form.tsx:112-112
Timestamp: 2025-12-22T19:50:04.233Z
Learning: When importing existing local repositories in the repositories service, users provide the full path (path + name) to an existing restic repository. Restic itself validates whether a valid repository exists at that location, so server-side path sanitization is not required for the import flow—the operation will fail if the path doesn't point to a valid restic repo.

Applied to files:

  • app/server/modules/repositories/repositories.service.ts
📚 Learning: 2025-12-28T17:31:39.171Z
Learnt from: CR
Repo: nicotsx/zerobyte PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-28T17:31:39.171Z
Learning: Applies to app/server/utils/restic.ts : Restic password file must have 0600 permissions and should never be exposed

Applied to files:

  • app/server/modules/repositories/repositories.service.ts
📚 Learning: 2025-12-28T17:31:39.171Z
Learnt from: CR
Repo: nicotsx/zerobyte PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-28T17:31:39.171Z
Learning: Applies to app/server/utils/restic.ts : Restic password file is stored in `/var/lib/zerobyte/restic/password` and auto-generated on first run

Applied to files:

  • app/server/modules/repositories/repositories.service.ts
📚 Learning: 2025-12-17T15:54:44.265Z
Learnt from: tvarohohlavy
Repo: nicotsx/zerobyte PR: 92
File: README.md:0-0
Timestamp: 2025-12-17T15:54:44.265Z
Learning: For ZeroByte PR #92, the export configuration feature is separate from import functionality—import is out of scope and handled in a separate PR #93. Documentation for the export feature should focus only on export behavior and not include import-related caveats.

Applied to files:

  • app/server/modules/repositories/repositories.service.ts
📚 Learning: 2025-12-28T17:31:39.171Z
Learnt from: CR
Repo: nicotsx/zerobyte PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-28T17:31:39.171Z
Learning: Applies to **/*.{ts,tsx,js,jsx,json} : Use Biome for code formatting and linting with `bunx biome check --write .`, format only with `bunx biome format --write .`, or lint with `bunx biome lint .`

Applied to files:

  • app/server/modules/repositories/repositories.service.ts
🧬 Code graph analysis (2)
app/server/modules/backups/backups.service.ts (2)
app/server/utils/id.ts (2)
  • isValidShortId (10-13)
  • generateShortId (5-8)
app/server/db/schema.ts (1)
  • backupSchedulesTable (70-102)
app/server/modules/repositories/repositories.service.ts (4)
app/schemas/restic.ts (2)
  • RepositoryConfig (95-95)
  • CompressionMode (103-103)
app/server/utils/id.ts (2)
  • isValidShortId (10-13)
  • generateShortId (5-8)
app/server/db/schema.ts (1)
  • repositoriesTable (51-63)
app/server/utils/restic.ts (1)
  • restic (836-849)
🪛 ast-grep (0.40.3)
app/server/utils/id.ts

[warning] 10-10: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns.
Context: new RegExp(^[A-Za-z0-9_-]{${length}}$)
Note: [CWE-1333] Inefficient Regular Expression Complexity [REFERENCES]
- https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS
- https://cwe.mitre.org/data/definitions/1333.html

(regexp-from-variable)

🔇 Additional comments (2)
app/server/utils/id.ts (1)

10-13: Static analysis false positive: regex is safe from ReDoS.

The static analysis tool flagged the RegExp constructor as a potential ReDoS risk. However, this is a false positive because:

  1. The length parameter is numeric (defaults to SHORT_ID_LENGTH = 8), not user-supplied string input
  2. The pattern ^[A-Za-z0-9_-]{n}$ is a simple character class with a fixed quantifier—no nested quantifiers or backtracking
  3. Time complexity is O(n) where n is the string length being tested

The regex safely validates that a string contains exactly length base64url characters.

app/server/modules/backups/backups.service.ts (1)

98-115: Use BadRequestError for consistent error handling.

Line 102 throws a generic Error for invalid shortId format, while line 108 correctly uses ConflictError for duplicates. For consistency with the rest of the service (e.g., lines 87, 170), invalid input should throw BadRequestError.

🔎 Proposed fix
 	if (providedShortId) {
 		if (!isValidShortId(providedShortId)) {
-			throw new Error(`Invalid shortId format: '${providedShortId}'. Must be 8 base64url characters.`);
+			throw new BadRequestError(`Invalid shortId format: '${providedShortId}'. Must be 8 base64url characters.`);
 		}
 		const shortIdInUse = await db.query.backupSchedulesTable.findFirst({

Likely an incorrect or invalid review comment.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (6)
app/server/cli/commands/import-config.ts (1)

134-137: Add .js extensions to dynamic imports for ESM compatibility.

Per the coding guidelines, imports must include file extensions when targeting Node/Bun with "type": "module". Dynamic imports should also follow this convention.

🔎 Proposed fix
-			const { runDbMigrations } = await import("../../db/db");
+			const { runDbMigrations } = await import("../../db/db.js");
 			runDbMigrations();

-			const { applyConfigImport } = await import("../../modules/lifecycle/config-import");
+			const { applyConfigImport } = await import("../../modules/lifecycle/config-import.js");
app/server/modules/lifecycle/config-import.ts (5)

4-14: Add .js extensions to static imports.

Static imports from relative paths require the .js extension when "type": "module" is set. These will cause runtime errors.

🔎 Proposed fix
-import { db } from "../../db/db";
+import { db } from "../../db/db.js";
 import {
 	usersTable,
 	volumesTable,
 	repositoriesTable,
 	backupSchedulesTable,
 	notificationDestinationsTable,
-} from "../../db/schema";
-import { logger } from "../../utils/logger";
-import { toError } from "../../utils/errors";
-import { volumeService } from "../volumes/volume.service";
+} from "../../db/schema.js";
+import { logger } from "../../utils/logger.js";
+import { toError } from "../../utils/errors.js";
+import { volumeService } from "../volumes/volume.service.js";

222-223: Add .js extensions to dynamic imports.

🔎 Proposed fix
-	const repoServiceModule = await import("../repositories/repositories.service");
-	const { buildRepoUrl, restic } = await import("../../utils/restic");
+	const repoServiceModule = await import("../repositories/repositories.service.js");
+	const { buildRepoUrl, restic } = await import("../../utils/restic.js");

313-313: Add .js extension to dynamic import.

🔎 Proposed fix
-	const notificationsServiceModule = await import("../notifications/notifications.service");
+	const notificationsServiceModule = await import("../notifications/notifications.service.js");

474-475: Add .js extensions to dynamic imports.

🔎 Proposed fix
-	const backupServiceModule = await import("../backups/backups.service");
-	const notificationsServiceModule = await import("../notifications/notifications.service");
+	const backupServiceModule = await import("../backups/backups.service.js");
+	const notificationsServiceModule = await import("../notifications/notifications.service.js");

692-692: Add .js extension to dynamic import.

🔎 Proposed fix
-		const { authService } = await import("../auth/auth.service");
+		const { authService } = await import("../auth/auth.service.js");
🧹 Nitpick comments (1)
app/server/cli/commands/import-config.ts (1)

106-130: Consider enhancing dry-run validation feedback.

The dry-run mode validates JSON structure and counts items, but doesn't validate individual item schemas (e.g., required fields, valid backends). This is fine for a quick check, but users might expect deeper validation.

Consider adding a note in the output or documentation clarifying that dry-run only validates JSON syntax and structure, not the semantic correctness of each item.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4889b85 and 24f850b.

📒 Files selected for processing (3)
  • app/server/cli/commands/import-config.ts
  • app/server/modules/lifecycle/config-import.ts
  • app/server/utils/errors.ts
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{ts,tsx,js,jsx,json}

📄 CodeRabbit inference engine (AGENTS.md)

Use Biome for code formatting and linting with bunx biome check --write ., format only with bunx biome format --write ., or lint with bunx biome lint .

Files:

  • app/server/utils/errors.ts
  • app/server/cli/commands/import-config.ts
  • app/server/modules/lifecycle/config-import.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx,js,jsx}: Use tabs (not spaces) for indentation with a line width of 120 characters
Use double quotes for strings
Do not auto-organize imports - imports organization is disabled in Biome
All imports must include file extensions when targeting Node/Bun, as the project uses "type": "module"

Files:

  • app/server/utils/errors.ts
  • app/server/cli/commands/import-config.ts
  • app/server/modules/lifecycle/config-import.ts
app/server/modules/**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

Server follows a modular service-oriented architecture with controller-service-database pattern in each module

Files:

  • app/server/modules/lifecycle/config-import.ts
🧠 Learnings (14)
📓 Common learnings
Learnt from: tvarohohlavy
Repo: nicotsx/zerobyte PR: 92
File: README.md:0-0
Timestamp: 2025-12-17T15:54:44.265Z
Learning: For ZeroByte PR #92, the export configuration feature is separate from import functionality—import is out of scope and handled in a separate PR #93. Documentation for the export feature should focus only on export behavior and not include import-related caveats.
📚 Learning: 2025-12-28T17:31:39.171Z
Learnt from: CR
Repo: nicotsx/zerobyte PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-28T17:31:39.171Z
Learning: Applies to **/*.{ts,tsx,js,jsx} : All imports must include file extensions when targeting Node/Bun, as the project uses `"type": "module"`

Applied to files:

  • app/server/cli/commands/import-config.ts
  • app/server/modules/lifecycle/config-import.ts
📚 Learning: 2025-12-28T17:31:39.171Z
Learnt from: CR
Repo: nicotsx/zerobyte PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-28T17:31:39.171Z
Learning: Applies to **/*.{ts,tsx,js,jsx} : Do not auto-organize imports - imports organization is disabled in Biome

Applied to files:

  • app/server/cli/commands/import-config.ts
  • app/server/modules/lifecycle/config-import.ts
📚 Learning: 2025-12-28T17:31:39.171Z
Learnt from: CR
Repo: nicotsx/zerobyte PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-28T17:31:39.171Z
Learning: Applies to **/*.{ts,tsx,js,jsx,json} : Use Biome for code formatting and linting with `bunx biome check --write .`, format only with `bunx biome format --write .`, or lint with `bunx biome lint .`

Applied to files:

  • app/server/cli/commands/import-config.ts
📚 Learning: 2025-12-01T22:25:20.502Z
Learnt from: tvarohohlavy
Repo: nicotsx/zerobyte PR: 92
File: app/client/components/export-dialog.tsx:189-196
Timestamp: 2025-12-01T22:25:20.502Z
Learning: Volume secrets in ZeroByte are not yet encrypted in the database (stored as cleartext) and are excluded from the export dialog's secrets handling. The export logic relies on the `isEncrypted()` helper function that checks for the "encv1" encryption prefix, so cleartext volume secrets cannot be automatically detected for exclusion. Once volume secret encryption is implemented, the export logic for volumes will need to be completed to enable proper secrets handling options.

Applied to files:

  • app/server/modules/lifecycle/config-import.ts
📚 Learning: 2025-12-28T17:31:39.171Z
Learnt from: CR
Repo: nicotsx/zerobyte PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-28T17:31:39.171Z
Learning: Applies to app/server/utils/restic.ts : Update `buildEnv()` in `app/server/utils/restic.ts` to handle credentials and configuration when adding a new repository backend

Applied to files:

  • app/server/modules/lifecycle/config-import.ts
📚 Learning: 2025-12-17T15:54:44.265Z
Learnt from: tvarohohlavy
Repo: nicotsx/zerobyte PR: 92
File: README.md:0-0
Timestamp: 2025-12-17T15:54:44.265Z
Learning: In ZeroByte, historically some sensitive data from past versions might not be encrypted in the database, and nested configuration is sometimes present in config fields which will be exported as-is without special handling by the export functionality.

Applied to files:

  • app/server/modules/lifecycle/config-import.ts
📚 Learning: 2025-12-28T17:31:39.171Z
Learnt from: CR
Repo: nicotsx/zerobyte PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-28T17:31:39.171Z
Learning: Applies to app/server/utils/restic.ts : Update `buildRepoUrl()` in `app/server/utils/restic.ts` when adding a new repository backend

Applied to files:

  • app/server/modules/lifecycle/config-import.ts
📚 Learning: 2025-12-22T19:50:04.233Z
Learnt from: nicotsx
Repo: nicotsx/zerobyte PR: 197
File: app/client/modules/repositories/components/create-repository-form.tsx:112-112
Timestamp: 2025-12-22T19:50:04.233Z
Learning: When importing existing local repositories in the repositories service, users provide the full path (path + name) to an existing restic repository. Restic itself validates whether a valid repository exists at that location, so server-side path sanitization is not required for the import flow—the operation will fail if the path doesn't point to a valid restic repo.

Applied to files:

  • app/server/modules/lifecycle/config-import.ts
📚 Learning: 2025-12-28T17:31:39.171Z
Learnt from: CR
Repo: nicotsx/zerobyte PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-28T17:31:39.171Z
Learning: Applies to app/server/modules/backends/**/*.ts : Each volume backend (NFS, SMB, WebDAV, SFTP, directory) must implement mounting logic using system tools

Applied to files:

  • app/server/modules/lifecycle/config-import.ts
📚 Learning: 2025-12-28T17:31:39.171Z
Learnt from: CR
Repo: nicotsx/zerobyte PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-28T17:31:39.171Z
Learning: Applies to app/server/modules/**/*.ts : Server follows a modular service-oriented architecture with controller-service-database pattern in each module

Applied to files:

  • app/server/modules/lifecycle/config-import.ts
📚 Learning: 2025-12-28T17:31:39.171Z
Learnt from: CR
Repo: nicotsx/zerobyte PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-28T17:31:39.171Z
Learning: Applies to app/server/jobs/**/*.ts : New scheduled jobs must extend `Job` class, implement `run()` method, and be registered in `app/server/modules/lifecycle/startup.ts` with a cron expression

Applied to files:

  • app/server/modules/lifecycle/config-import.ts
📚 Learning: 2025-12-28T17:31:39.171Z
Learnt from: CR
Repo: nicotsx/zerobyte PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-28T17:31:39.171Z
Learning: Applies to app/drizzle/**/*.{ts,json} : If an automated migration is incorrect, remove all associated entries from `_journal.json` and newly created files in `app/drizzle/` before re-generating the migration

Applied to files:

  • app/server/modules/lifecycle/config-import.ts
📚 Learning: 2025-11-30T15:18:38.913Z
Learnt from: tvarohohlavy
Repo: nicotsx/zerobyte PR: 89
File: app/server/modules/notifications/builders/telegram.ts:3-8
Timestamp: 2025-11-30T15:18:38.913Z
Learning: In app/server/modules/notifications/builders/telegram.ts, the Telegram bot token in Shoutrrr URLs must NOT be URL-encoded. When passed to the Shoutrrr CLI, encoding the bot token (e.g., encoding the colon as %3A) causes "failed to initialize service" errors. The token should be used raw in the URL userinfo section. Chat IDs in the channels query parameter can optionally be encoded but work fine unencoded as well.

Applied to files:

  • app/server/modules/lifecycle/config-import.ts
🧬 Code graph analysis (1)
app/server/cli/commands/import-config.ts (4)
app/server/utils/errors.ts (1)
  • toError (21-21)
app/server/core/config.ts (1)
  • config (32-32)
app/server/db/db.ts (1)
  • runDbMigrations (16-30)
app/server/modules/lifecycle/config-import.ts (1)
  • applyConfigImport (815-821)
🔇 Additional comments (17)
app/server/utils/errors.ts (1)

21-22: LGTM!

Clean utility function that complements the existing toMessage helper. The implementation correctly handles the unknown type and provides consistent error normalization across the codebase.

app/server/cli/commands/import-config.ts (3)

8-14: LGTM!

Clean stdin reading implementation using async iteration.


16-37: LGTM!

Well-structured output abstraction that cleanly handles both JSON and human-readable modes. The never return type on error correctly indicates non-returning behavior.


39-62: LGTM!

The function correctly handles both stdin and file input paths. The precondition checks at lines 83-94 ensure either --config or --stdin is provided before this function is called.

app/server/modules/lifecycle/config-import.ts (13)

19-24: LGTM!

Clean type guard and array coercion utilities.


52-69: Environment variable interpolation works correctly.

The function handles missing env vars by warning and substituting empty strings. This is a reasonable default for partial imports, though sensitive fields (passwords, keys) may benefit from stricter validation in critical deployments.


103-118: LGTM!

Efficient parallel query approach for checking database emptiness. Good safety mechanism for recovery key overwrite protection.


120-176: LGTM!

Robust recovery key handling with proper validation (64-char hex), secure file permissions (0o600), and comprehensive safety checks preventing accidental overwrites of existing keys when data exists.


178-218: LGTM!

Clean implementation with proper duplicate detection using slugified names, shortId support for IaC reproducibility, and appropriate error handling.


258-277: Good defensive check for existing restic repositories.

The logic correctly detects when a location is already initialized as a restic repository and warns the user to set isExistingRepository: true if they want to import it. The debug logging on line 265 aids troubleshooting.


355-413: LGTM!

Flexible schema handling that accepts both volume/volumeName and repository/repositoryName keys improves config file ergonomics. The notification assignment builder correctly uses slugified lookups.


415-468: LGTM!

Correctly implements idempotent notification attachment with proper deduplication and merge semantics.


527-576: Good idempotent schedule handling.

The logic correctly skips existing schedules while still attempting to attach notifications and mirrors. Volume mount failures now properly increment warnings (line 544).


606-686: LGTM!

Mirrors attachment logic correctly follows the same idempotent pattern as notifications, with proper deduplication and merge semantics.


710-762: LGTM!

User import correctly handles both pre-hashed passwords (for migration/backup restore) and plaintext passwords (for IaC), with proper single-user enforcement via break after successful creation.


776-793: LGTM!

Clean orchestration with appropriate early termination on recovery key errors. The import order (recovery key → volumes → repositories → notifications → schedules → users) ensures dependencies are available when needed.


795-821: LGTM!

Good summary logging with appropriate severity levels based on the result type (errors → error level, warnings → warn level, success → info level).

- Add app/schemas/config-import.ts with ArkType schemas for upfront validation
- Replace runtime type checks with schema-based validation in config-import.ts
- Return validation errors early with detailed path and message info
- Export retentionPolicySchema from backups.dto.ts for reuse
- Update CLI to handle validation errors in both dry-run and import modes
- Fix README mirrors examples to use existing repository names
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (4)
app/server/cli/commands/import-config.ts (1)

53-58: Potential UX issue: empty config path resolves to current directory.

When options.config is undefined (which shouldn't happen given the check at line 83-89), path.resolve(process.cwd(), "") resolves to the current working directory. While the subsequent fs.access check will fail with a directory, the error message would be confusing.

Consider adding explicit handling:

🔎 Proposed fix
-	const configPath = path.resolve(process.cwd(), options.config ?? "");
+	if (!options.config) {
+		out.error("Config path is required");
+	}
+	const configPath = path.resolve(process.cwd(), options.config);
app/server/modules/lifecycle/config-import.ts (2)

40-57: Consider documenting the lenient env var interpolation behavior.

The function warns and substitutes empty strings for undefined variables. While this enables partial imports, users may not realize a typo in ${VAR_NAME} silently becomes an empty value. The warning log helps, but consider adding a note in the README about this behavior.


541-605: Type mismatch in repoByName map definition.

The map is typed as Map<string, { id: string; name: string; config: RepositoryConfig }> at line 545, but repoByName is created from db.query.repositoriesTable.findMany() at line 437 which returns database rows where config is stored as JSON (likely unknown or a broader type). The actual usage only accesses id and name, so this works at runtime but the type annotation is misleading.

🔎 Proposed fix
 async function attachScheduleMirrors(
 	scheduleId: number,
 	scheduleName: string,
 	mirrors: ScheduleMirrorImport[],
-	repoByName: Map<string, { id: string; name: string; config: RepositoryConfig }>,
+	repoByName: Map<string, { id: string; name: string }>,
 	backupServiceModule: typeof import("../backups/backups.service"),
 ): Promise<ImportResult> {
app/schemas/config-import.ts (1)

77-82: Schema allows both password and passwordHash simultaneously.

The user import schema accepts both password and passwordHash as optional fields without enforcing mutual exclusivity. While the import logic at config-import.ts:630-664 correctly prioritizes passwordHash and then falls back to password, the schema could be stricter.

This is a low-priority improvement since the runtime logic handles it correctly.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 24f850b and 8591432.

📒 Files selected for processing (5)
  • app/schemas/config-import.ts
  • app/server/cli/commands/import-config.ts
  • app/server/modules/backups/backups.dto.ts
  • app/server/modules/lifecycle/config-import.ts
  • examples/config-file-import/README.md
🧰 Additional context used
📓 Path-based instructions (4)
**/*.{ts,tsx,js,jsx,json}

📄 CodeRabbit inference engine (AGENTS.md)

Use Biome for code formatting and linting with bunx biome check --write ., format only with bunx biome format --write ., or lint with bunx biome lint .

Files:

  • app/schemas/config-import.ts
  • app/server/modules/backups/backups.dto.ts
  • app/server/cli/commands/import-config.ts
  • app/server/modules/lifecycle/config-import.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx,js,jsx}: Use tabs (not spaces) for indentation with a line width of 120 characters
Use double quotes for strings
Do not auto-organize imports - imports organization is disabled in Biome
All imports must include file extensions when targeting Node/Bun, as the project uses "type": "module"

Files:

  • app/schemas/config-import.ts
  • app/server/modules/backups/backups.dto.ts
  • app/server/cli/commands/import-config.ts
  • app/server/modules/lifecycle/config-import.ts
app/schemas/**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

Prefer ArkType over Zod for runtime schema validation

Files:

  • app/schemas/config-import.ts
app/server/modules/**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

Server follows a modular service-oriented architecture with controller-service-database pattern in each module

Files:

  • app/server/modules/backups/backups.dto.ts
  • app/server/modules/lifecycle/config-import.ts
🧠 Learnings (18)
📓 Common learnings
Learnt from: tvarohohlavy
Repo: nicotsx/zerobyte PR: 92
File: README.md:0-0
Timestamp: 2025-12-17T15:54:44.265Z
Learning: For ZeroByte PR #92, the export configuration feature is separate from import functionality—import is out of scope and handled in a separate PR #93. Documentation for the export feature should focus only on export behavior and not include import-related caveats.
📚 Learning: 2025-12-28T17:31:39.171Z
Learnt from: CR
Repo: nicotsx/zerobyte PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-28T17:31:39.171Z
Learning: Applies to app/schemas/volumes.ts : Add schemas for new volume backends to `app/schemas/volumes.ts` and update `volumeConfigSchema` discriminated union

Applied to files:

  • app/schemas/config-import.ts
  • app/server/modules/backups/backups.dto.ts
📚 Learning: 2025-12-28T17:31:39.171Z
Learnt from: CR
Repo: nicotsx/zerobyte PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-28T17:31:39.171Z
Learning: Applies to app/schemas/**/*.ts : Prefer ArkType over Zod for runtime schema validation

Applied to files:

  • app/schemas/config-import.ts
📚 Learning: 2025-12-28T17:31:39.171Z
Learnt from: CR
Repo: nicotsx/zerobyte PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-28T17:31:39.171Z
Learning: Applies to app/server/modules/repositories/repositories.dto.ts : Add DTO schemas in `app/server/modules/repositories/repositories.dto.ts` when adding a new repository backend

Applied to files:

  • app/schemas/config-import.ts
  • app/server/modules/backups/backups.dto.ts
📚 Learning: 2025-12-28T17:31:39.171Z
Learnt from: CR
Repo: nicotsx/zerobyte PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-28T17:31:39.171Z
Learning: Applies to **/*.{ts,tsx,js,jsx} : Do not auto-organize imports - imports organization is disabled in Biome

Applied to files:

  • app/server/cli/commands/import-config.ts
  • app/server/modules/lifecycle/config-import.ts
📚 Learning: 2025-12-28T17:31:39.171Z
Learnt from: CR
Repo: nicotsx/zerobyte PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-28T17:31:39.171Z
Learning: Applies to **/*.{ts,tsx,js,jsx,json} : Use Biome for code formatting and linting with `bunx biome check --write .`, format only with `bunx biome format --write .`, or lint with `bunx biome lint .`

Applied to files:

  • app/server/cli/commands/import-config.ts
📚 Learning: 2025-12-17T15:54:44.265Z
Learnt from: tvarohohlavy
Repo: nicotsx/zerobyte PR: 92
File: README.md:0-0
Timestamp: 2025-12-17T15:54:44.265Z
Learning: For ZeroByte PR #92, the export configuration feature is separate from import functionality—import is out of scope and handled in a separate PR #93. Documentation for the export feature should focus only on export behavior and not include import-related caveats.

Applied to files:

  • app/server/cli/commands/import-config.ts
  • examples/config-file-import/README.md
📚 Learning: 2025-12-28T17:31:39.171Z
Learnt from: CR
Repo: nicotsx/zerobyte PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-28T17:31:39.171Z
Learning: Applies to app/server/utils/restic.ts : Update `buildEnv()` in `app/server/utils/restic.ts` to handle credentials and configuration when adding a new repository backend

Applied to files:

  • app/server/modules/lifecycle/config-import.ts
📚 Learning: 2025-12-01T22:25:20.502Z
Learnt from: tvarohohlavy
Repo: nicotsx/zerobyte PR: 92
File: app/client/components/export-dialog.tsx:189-196
Timestamp: 2025-12-01T22:25:20.502Z
Learning: Volume secrets in ZeroByte are not yet encrypted in the database (stored as cleartext) and are excluded from the export dialog's secrets handling. The export logic relies on the `isEncrypted()` helper function that checks for the "encv1" encryption prefix, so cleartext volume secrets cannot be automatically detected for exclusion. Once volume secret encryption is implemented, the export logic for volumes will need to be completed to enable proper secrets handling options.

Applied to files:

  • app/server/modules/lifecycle/config-import.ts
📚 Learning: 2025-12-17T15:54:44.265Z
Learnt from: tvarohohlavy
Repo: nicotsx/zerobyte PR: 92
File: README.md:0-0
Timestamp: 2025-12-17T15:54:44.265Z
Learning: In ZeroByte, historically some sensitive data from past versions might not be encrypted in the database, and nested configuration is sometimes present in config fields which will be exported as-is without special handling by the export functionality.

Applied to files:

  • app/server/modules/lifecycle/config-import.ts
  • examples/config-file-import/README.md
📚 Learning: 2025-12-28T17:31:39.171Z
Learnt from: CR
Repo: nicotsx/zerobyte PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-28T17:31:39.171Z
Learning: Applies to app/server/utils/restic.ts : Update `buildRepoUrl()` in `app/server/utils/restic.ts` when adding a new repository backend

Applied to files:

  • app/server/modules/lifecycle/config-import.ts
📚 Learning: 2025-12-22T19:50:04.233Z
Learnt from: nicotsx
Repo: nicotsx/zerobyte PR: 197
File: app/client/modules/repositories/components/create-repository-form.tsx:112-112
Timestamp: 2025-12-22T19:50:04.233Z
Learning: When importing existing local repositories in the repositories service, users provide the full path (path + name) to an existing restic repository. Restic itself validates whether a valid repository exists at that location, so server-side path sanitization is not required for the import flow—the operation will fail if the path doesn't point to a valid restic repo.

Applied to files:

  • app/server/modules/lifecycle/config-import.ts
📚 Learning: 2025-12-28T17:31:39.171Z
Learnt from: CR
Repo: nicotsx/zerobyte PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-28T17:31:39.171Z
Learning: Applies to app/server/modules/backends/**/*.ts : Each volume backend (NFS, SMB, WebDAV, SFTP, directory) must implement mounting logic using system tools

Applied to files:

  • app/server/modules/lifecycle/config-import.ts
📚 Learning: 2025-12-28T17:31:39.171Z
Learnt from: CR
Repo: nicotsx/zerobyte PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-28T17:31:39.171Z
Learning: Applies to app/server/modules/**/*.ts : Server follows a modular service-oriented architecture with controller-service-database pattern in each module

Applied to files:

  • app/server/modules/lifecycle/config-import.ts
📚 Learning: 2025-12-28T17:31:39.171Z
Learnt from: CR
Repo: nicotsx/zerobyte PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-28T17:31:39.171Z
Learning: Applies to app/server/jobs/**/*.ts : New scheduled jobs must extend `Job` class, implement `run()` method, and be registered in `app/server/modules/lifecycle/startup.ts` with a cron expression

Applied to files:

  • app/server/modules/lifecycle/config-import.ts
📚 Learning: 2025-12-28T17:31:39.171Z
Learnt from: CR
Repo: nicotsx/zerobyte PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-28T17:31:39.171Z
Learning: Applies to app/drizzle/**/*.{ts,json} : If an automated migration is incorrect, remove all associated entries from `_journal.json` and newly created files in `app/drizzle/` before re-generating the migration

Applied to files:

  • app/server/modules/lifecycle/config-import.ts
📚 Learning: 2025-11-30T15:18:38.913Z
Learnt from: tvarohohlavy
Repo: nicotsx/zerobyte PR: 89
File: app/server/modules/notifications/builders/telegram.ts:3-8
Timestamp: 2025-11-30T15:18:38.913Z
Learning: In app/server/modules/notifications/builders/telegram.ts, the Telegram bot token in Shoutrrr URLs must NOT be URL-encoded. When passed to the Shoutrrr CLI, encoding the bot token (e.g., encoding the colon as %3A) causes "failed to initialize service" errors. The token should be used raw in the URL userinfo section. Chat IDs in the channels query parameter can optionally be encoded but work fine unencoded as well.

Applied to files:

  • app/server/modules/lifecycle/config-import.ts
📚 Learning: 2025-12-28T17:31:39.171Z
Learnt from: CR
Repo: nicotsx/zerobyte PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-28T17:31:39.171Z
Learning: Applies to app/server/utils/restic.ts : Restic password file is stored in `/var/lib/zerobyte/restic/password` and auto-generated on first run

Applied to files:

  • examples/config-file-import/README.md
🧬 Code graph analysis (2)
app/schemas/config-import.ts (4)
app/schemas/volumes.ts (1)
  • volumeConfigSchema (79-79)
app/schemas/restic.ts (1)
  • repositoryConfigSchema (93-93)
app/schemas/notifications.ts (1)
  • notificationConfigSchema (88-88)
app/server/modules/backups/backups.dto.ts (1)
  • retentionPolicySchema (6-14)
app/server/cli/commands/import-config.ts (3)
app/server/utils/errors.ts (1)
  • toError (21-21)
app/server/modules/lifecycle/config-import.ts (2)
  • validateConfig (746-748)
  • applyConfigImport (726-740)
app/server/db/db.ts (1)
  • runDbMigrations (16-30)
🪛 LanguageTool
examples/config-file-import/README.md

[style] ~440-~440: To form a complete sentence, be sure to include a subject or ‘there’.
Context: ...tifications (detailed) notifications can be strings (destination names) or objec...

(MISSING_IT_THERE)

🔇 Additional comments (22)
app/server/modules/backups/backups.dto.ts (1)

6-14: LGTM! Exporting the schema enables proper reuse.

Exporting retentionPolicySchema allows the config-import module to reference the canonical retention policy definition, avoiding schema duplication.

examples/config-file-import/README.md (1)

1-519: Comprehensive and well-structured documentation.

The README provides clear examples for all supported resource types, explains both interpolation mechanisms (${VAR} vs env://), and documents CLI options thoroughly. The create-only semantics are well-explained.

app/server/cli/commands/import-config.ts (5)

8-14: LGTM! Clean stdin reading implementation.

The async iterator pattern for reading stdin chunks is idiomatic and handles the buffer concatenation correctly.


16-37: LGTM! Well-designed output abstraction.

The createOutput helper cleanly separates JSON and human-readable output modes, with the error method correctly exiting with code 1.


64-94: LGTM! Clear command definition with appropriate options.

The command properly defines mutually exclusive input sources and provides helpful usage guidance when neither is specified.


106-144: LGTM! Dry-run mode provides useful validation feedback.

The dry-run implementation correctly validates the config schema and reports item counts without making any changes.


146-175: LGTM! Import execution with proper error handling.

The import flow correctly runs DB migrations before applying the config, handles validation errors distinctly from runtime errors, and exits with appropriate codes.

app/server/modules/lifecycle/config-import.ts (10)

1-29: LGTM! Well-organized imports and type definitions.

The imports are logically grouped and the module correctly uses ArkType schemas for validation as per coding guidelines.


72-92: LGTM! Clean schema validation with helpful error paths.

The parseImportConfig function properly handles wrapped config format, applies env interpolation before validation, and provides detailed error paths for debugging.


122-175: LGTM! Robust recovery key handling with safety checks.

The function correctly:

  • Skips if key already matches
  • Requires explicit --overwrite-recovery-key flag for replacement
  • Verifies database is empty before allowing overwrite (preventing data loss)
  • Sets proper file permissions (0o600)

177-211: LGTM! Volume import with proper duplicate detection.

The function correctly slugifies names before checking for duplicates, passes optional shortId for IaC reproducibility, and handles autoRemount as a post-creation update.


249-266: Improved: Now logs repo existence check failures for debugging.

The debug logging at line 254 addresses the earlier review concern about silently swallowing errors during repository existence checks.


343-372: LGTM! Notification assignment builder handles both string and object formats.

The function correctly processes both simple destination names and detailed notification objects with per-event settings.


429-539: LGTM! Schedule import with proper dependency resolution.

The function correctly:

  • Validates volume and repository existence before creating schedules
  • Generates default schedule names from volume+repository if not provided
  • Skips existing schedules but still processes notification/mirror attachments
  • Mounts unmounted volumes before backup schedule creation

607-676: LGTM! User import with proper single-user handling.

The function correctly:

  • Skips if users already exist
  • Warns when multiple users are provided (Zerobyte limitation)
  • Supports both plaintext passwords and pre-hashed passwords
  • Sets hasDownloadedResticPassword based on recovery key presence

682-699: LGTM! Import orchestration with proper sequencing.

The runImport function correctly stops early if recovery key has errors (preventing orphaned resources) and follows the documented import order.


726-748: LGTM! Clean public API for config import.

The applyConfigImport and validateConfig exports provide a well-designed interface for the CLI and potential future integrations.

app/schemas/config-import.ts (5)

1-5: LGTM! Proper schema imports following coding guidelines.

Using ArkType and importing existing validated schemas (volumeConfigSchema, repositoryConfigSchema, etc.) ensures consistency with the rest of the codebase.


12-13: LGTM! ShortId schema matches expected format.

The regex /^[A-Za-z0-9_-]{8}$/ correctly validates 8-character base64url strings used for reproducible IaC identifiers.


16-36: LGTM! Resource import schemas are well-defined.

Volume, repository, and notification schemas properly:

  • Require non-empty names (string>=1)
  • Make shortId optional for IaC flexibility
  • Strip undeclared keys for forward compatibility

59-74: LGTM! Backup schedule schema with comprehensive options.

The schema supports all schedule configuration options including retention policy, patterns, notifications, and mirrors.


94-114: LGTM! Root config schema and type exports.

The importConfigSchema properly aggregates all resource arrays as optional (supporting partial imports) and exports all necessary types for consumers.

@tvarohohlavy
Copy link
Contributor Author

tvarohohlavy commented Jan 2, 2026

@nicotsx
I have changed it so it can be triggered only via cli now and added lots of enhancements including ArkType validation.
I have tested it with both full config exports from the other PR and also with those excluding metadata.
To have the imports idempotent imports do not allow duplicate repository names.
I feel its now ready, but lets see what do you think.
Hope you will like it : ]

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Fix all issues with AI Agents 🤖
In @app/server/cli/index.ts:
- Line 2: Update the import statement that brings in importConfigCommand in
index.ts to use a Node-compatible module specifier by adding the .js extension
to the imported module path; locate the line importing importConfigCommand and
change its module specifier to include the .js extension so the import conforms
to "type: module" ESM rules.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/server/modules/repositories/repositories.service.ts (1)

31-68: CRITICAL: Missing encryption guards cause double-encryption.

The encryptConfig function encrypts sensitive fields unconditionally without checking if they're already encrypted. When importing configurations (e.g., from exports or migrations), fields may already be encrypted, leading to double-encryption that corrupts the values and renders repositories unusable.

As noted in the PR objectives, the maintainer flagged this as a critical issue and recommended adding guards using cryptoUtils.isEncrypted() for each sensitive field before encryption.

🔎 Proposed fix adding encryption guards
 const encryptConfig = async (config: RepositoryConfig): Promise<RepositoryConfig> => {
 	const encryptedConfig: Record<string, string | boolean | number> = { ...config };
 
-	if (config.customPassword) {
+	if (config.customPassword && !cryptoUtils.isEncrypted(config.customPassword)) {
 		encryptedConfig.customPassword = await cryptoUtils.sealSecret(config.customPassword);
 	}
 
-	if (config.cacert) {
+	if (config.cacert && !cryptoUtils.isEncrypted(config.cacert)) {
 		encryptedConfig.cacert = await cryptoUtils.sealSecret(config.cacert);
 	}
 
 	switch (config.backend) {
 		case "s3":
 		case "r2":
-			encryptedConfig.accessKeyId = await cryptoUtils.sealSecret(config.accessKeyId);
-			encryptedConfig.secretAccessKey = await cryptoUtils.sealSecret(config.secretAccessKey);
+			if (!cryptoUtils.isEncrypted(config.accessKeyId)) {
+				encryptedConfig.accessKeyId = await cryptoUtils.sealSecret(config.accessKeyId);
+			}
+			if (!cryptoUtils.isEncrypted(config.secretAccessKey)) {
+				encryptedConfig.secretAccessKey = await cryptoUtils.sealSecret(config.secretAccessKey);
+			}
 			break;
 		case "gcs":
-			encryptedConfig.credentialsJson = await cryptoUtils.sealSecret(config.credentialsJson);
+			if (!cryptoUtils.isEncrypted(config.credentialsJson)) {
+				encryptedConfig.credentialsJson = await cryptoUtils.sealSecret(config.credentialsJson);
+			}
 			break;
 		case "azure":
-			encryptedConfig.accountKey = await cryptoUtils.sealSecret(config.accountKey);
+			if (!cryptoUtils.isEncrypted(config.accountKey)) {
+				encryptedConfig.accountKey = await cryptoUtils.sealSecret(config.accountKey);
+			}
 			break;
 		case "rest":
-			if (config.username) {
+			if (config.username && !cryptoUtils.isEncrypted(config.username)) {
 				encryptedConfig.username = await cryptoUtils.sealSecret(config.username);
 			}
-			if (config.password) {
+			if (config.password && !cryptoUtils.isEncrypted(config.password)) {
 				encryptedConfig.password = await cryptoUtils.sealSecret(config.password);
 			}
 			break;
 		case "sftp":
-			encryptedConfig.privateKey = await cryptoUtils.sealSecret(config.privateKey);
+			if (!cryptoUtils.isEncrypted(config.privateKey)) {
+				encryptedConfig.privateKey = await cryptoUtils.sealSecret(config.privateKey);
+			}
 			break;
 	}
 
 	return encryptedConfig as RepositoryConfig;
 };

Based on PR objectives, this was flagged as critical by the maintainer.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 88d5e17 and 08f7dca.

📒 Files selected for processing (4)
  • README.md
  • app/server/cli/index.ts
  • app/server/modules/backups/backups.service.ts
  • app/server/modules/repositories/repositories.service.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • README.md
🧰 Additional context used
📓 Path-based instructions (4)
**/*.{ts,tsx,js,jsx,json}

📄 CodeRabbit inference engine (AGENTS.md)

Use Biome for code formatting and linting with bunx biome check --write ., format only with bunx biome format --write ., or lint with bunx biome lint .

Files:

  • app/server/cli/index.ts
  • app/server/modules/backups/backups.service.ts
  • app/server/modules/repositories/repositories.service.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx,js,jsx}: Use tabs (not spaces) for indentation with a line width of 120 characters
Use double quotes for strings
Do not auto-organize imports - imports organization is disabled in Biome
All imports must include file extensions when targeting Node/Bun, as the project uses "type": "module"

Files:

  • app/server/cli/index.ts
  • app/server/modules/backups/backups.service.ts
  • app/server/modules/repositories/repositories.service.ts
app/server/modules/**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

Server follows a modular service-oriented architecture with controller-service-database pattern in each module

Files:

  • app/server/modules/backups/backups.service.ts
  • app/server/modules/repositories/repositories.service.ts
app/server/modules/repositories/**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

Local repositories are stored in /var/lib/zerobyte/repositories/<repo-name>

Files:

  • app/server/modules/repositories/repositories.service.ts
🧠 Learnings (11)
📓 Common learnings
Learnt from: tvarohohlavy
Repo: nicotsx/zerobyte PR: 92
File: README.md:0-0
Timestamp: 2025-12-17T15:54:44.265Z
Learning: For ZeroByte PR #92, the export configuration feature is separate from import functionality—import is out of scope and handled in a separate PR #93. Documentation for the export feature should focus only on export behavior and not include import-related caveats.
📚 Learning: 2025-12-03T19:48:55.119Z
Learnt from: nicotsx
Repo: nicotsx/zerobyte PR: 95
File: app/server/modules/backups/backups.service.ts:525-533
Timestamp: 2025-12-03T19:48:55.119Z
Learning: In app/server/modules/backups/backups.service.ts, the mirror copy operations use shared locks (acquireShared) for both source and mirror repositories because restic supports parallel write operations and concurrent backups with non-exclusive locks. Only maintenance operations like prune and rebuild-index require exclusive locks in restic.

Applied to files:

  • app/server/modules/backups/backups.service.ts
📚 Learning: 2025-12-28T17:31:39.171Z
Learnt from: CR
Repo: nicotsx/zerobyte PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-28T17:31:39.171Z
Learning: Applies to app/server/utils/restic.ts : Update `buildRepoUrl()` in `app/server/utils/restic.ts` when adding a new repository backend

Applied to files:

  • app/server/modules/backups/backups.service.ts
  • app/server/modules/repositories/repositories.service.ts
📚 Learning: 2025-12-28T17:31:39.171Z
Learnt from: CR
Repo: nicotsx/zerobyte PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-28T17:31:39.171Z
Learning: Applies to app/server/utils/restic.ts : Update `buildEnv()` in `app/server/utils/restic.ts` to handle credentials and configuration when adding a new repository backend

Applied to files:

  • app/server/modules/backups/backups.service.ts
  • app/server/modules/repositories/repositories.service.ts
📚 Learning: 2025-12-28T17:31:39.171Z
Learnt from: CR
Repo: nicotsx/zerobyte PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-28T17:31:39.171Z
Learning: Applies to app/server/modules/repositories/repositories.dto.ts : Add DTO schemas in `app/server/modules/repositories/repositories.dto.ts` when adding a new repository backend

Applied to files:

  • app/server/modules/repositories/repositories.service.ts
📚 Learning: 2025-12-28T17:31:39.171Z
Learnt from: CR
Repo: nicotsx/zerobyte PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-28T17:31:39.171Z
Learning: Applies to app/server/modules/repositories/**/*.ts : Local repositories are stored in `/var/lib/zerobyte/repositories/<repo-name>`

Applied to files:

  • app/server/modules/repositories/repositories.service.ts
📚 Learning: 2025-12-22T19:50:04.233Z
Learnt from: nicotsx
Repo: nicotsx/zerobyte PR: 197
File: app/client/modules/repositories/components/create-repository-form.tsx:112-112
Timestamp: 2025-12-22T19:50:04.233Z
Learning: When importing existing local repositories in the repositories service, users provide the full path (path + name) to an existing restic repository. Restic itself validates whether a valid repository exists at that location, so server-side path sanitization is not required for the import flow—the operation will fail if the path doesn't point to a valid restic repo.

Applied to files:

  • app/server/modules/repositories/repositories.service.ts
📚 Learning: 2025-12-28T17:31:39.171Z
Learnt from: CR
Repo: nicotsx/zerobyte PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-28T17:31:39.171Z
Learning: Applies to app/server/utils/restic.ts : Restic password file must have 0600 permissions and should never be exposed

Applied to files:

  • app/server/modules/repositories/repositories.service.ts
📚 Learning: 2025-12-28T17:31:39.171Z
Learnt from: CR
Repo: nicotsx/zerobyte PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-28T17:31:39.171Z
Learning: Applies to app/server/utils/restic.ts : Restic password file is stored in `/var/lib/zerobyte/restic/password` and auto-generated on first run

Applied to files:

  • app/server/modules/repositories/repositories.service.ts
📚 Learning: 2025-12-17T15:54:44.265Z
Learnt from: tvarohohlavy
Repo: nicotsx/zerobyte PR: 92
File: README.md:0-0
Timestamp: 2025-12-17T15:54:44.265Z
Learning: For ZeroByte PR #92, the export configuration feature is separate from import functionality—import is out of scope and handled in a separate PR #93. Documentation for the export feature should focus only on export behavior and not include import-related caveats.

Applied to files:

  • app/server/modules/repositories/repositories.service.ts
📚 Learning: 2025-12-28T17:31:39.171Z
Learnt from: CR
Repo: nicotsx/zerobyte PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-28T17:31:39.171Z
Learning: Applies to **/*.{ts,tsx,js,jsx,json} : Use Biome for code formatting and linting with `bunx biome check --write .`, format only with `bunx biome format --write .`, or lint with `bunx biome lint .`

Applied to files:

  • app/server/modules/repositories/repositories.service.ts
🧬 Code graph analysis (2)
app/server/cli/index.ts (1)
app/server/cli/commands/import-config.ts (1)
  • importConfigCommand (64-176)
app/server/modules/repositories/repositories.service.ts (4)
app/schemas/restic.ts (2)
  • RepositoryConfig (97-97)
  • CompressionMode (105-105)
app/server/utils/id.ts (2)
  • isValidShortId (10-13)
  • generateShortId (5-8)
app/server/db/schema.ts (1)
  • repositoriesTable (51-63)
app/server/utils/restic.ts (1)
  • restic (915-930)
🔇 Additional comments (7)
app/server/modules/backups/backups.service.ts (2)

18-18: LGTM: Import addition is clean.

The addition of isValidShortId to the imports is appropriate for the new validation logic.


86-86: LGTM: ShortId handling is well-implemented.

The validation and collision detection logic for providedShortId is correct and provides clear error messages. A unique constraint exists on the shortId column (shortId: text("short_id").notNull().unique() in the schema), which prevents race conditions between the collision check and insert at the database level. The implementation follows a consistent pattern with other modules.

app/server/modules/repositories/repositories.service.ts (4)

78-95: LGTM! ShortId validation logic is well-structured.

The shortId handling correctly:

  • Validates format using isValidShortId before use
  • Checks uniqueness against the database
  • Uses appropriate error types (BadRequestError for invalid format, ConflictError for duplicates)
  • Provides clear, actionable error messages
  • Falls back to generating a new shortId when none is provided

This supports the config import use case while maintaining data integrity.


104-120: LGTM! Repository existence validation is robust.

The pre-insertion validation correctly distinguishes between creating new repositories and importing existing ones:

  • Uses restic.snapshots() to verify both existence and accessibility
  • Throws ConflictError when attempting to create over an existing repo (with guidance to set isExistingRepository: true)
  • Throws BadRequestError when attempting to import a non-existent repo
  • Provides clear, actionable error messages to guide configuration fixes

This prevents data loss from accidental overwrites and catches misconfiguration early.


141-144: LGTM! Initialization condition is clearer.

The change from checking !config.isExistingRepository to !repoExists is more explicit and correct. Since the validation at lines 109-120 ensures consistency between the flag and the configuration, using the repoExists variable directly makes the initialization logic clearer and easier to understand.


98-100: Local repository directory naming is correct.

For new local repositories, using shortId as the repository name ensures unique directory paths at /var/lib/zerobyte/repositories/<shortId>. The check for !config.isExistingRepository correctly preserves the original name when importing existing repositories.

Based on learnings, local repositories are stored in /var/lib/zerobyte/repositories/<repo-name>.

app/server/cli/index.ts (1)

8-8: LGTM!

The command registration follows the established pattern and correctly integrates the new import-config command into the CLI.

@@ -1,9 +1,11 @@
import { Command } from "commander";
import { importConfigCommand } from "./commands/import-config";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash

echo "=== Checking package.json for type: module ==="
if [ -f package.json ]; then
  cat package.json | jq '.type // "not set"'
else
  echo "package.json not found"
fi

echo -e "\n=== Checking tsconfig.json module resolution ==="
if [ -f tsconfig.json ]; then
  cat tsconfig.json | jq '.compilerOptions.module, .compilerOptions.moduleResolution'
else
  echo "tsconfig.json not found"
fi

echo -e "\n=== Checking import patterns in TypeScript files (sample) ==="
rg -n 'from\s+["\x27]\./[^"\']*["\x27]' --type ts --max-count=20

echo -e "\n=== Checking app/server/cli/index.ts specifically ==="
cat -n app/server/cli/index.ts | head -15

Repository: nicotsx/zerobyte

Length of output: 334


🏁 Script executed:

#!/bin/bash

echo "=== Checking app/server/cli/index.ts ==="
cat -n app/server/cli/index.ts

echo -e "\n=== Checking import patterns (first 30 lines of TS files with imports) ==="
rg -n 'from.*["\x27]\.' --type ts --max-count=15

echo -e "\n=== Checking if any imports use file extensions ==="
rg -n 'from.*\.(ts|js)["\x27]' --type ts --max-count=15

Repository: nicotsx/zerobyte

Length of output: 50372


Add file extension to import statement.

The import on line 2 must include a file extension per the coding guideline: "All imports must include file extensions when targeting Node/Bun, as the project uses "type": "module"". Change to import { importConfigCommand } from "./commands/import-config.js";

Note: While the rest of the codebase omits extensions, this guideline is explicit and one file (openapi-ts.config.ts) demonstrates compliance by including .js.

🤖 Prompt for AI Agents
In @app/server/cli/index.ts around line 2, Update the import statement that
brings in importConfigCommand in index.ts to use a Node-compatible module
specifier by adding the .js extension to the imported module path; locate the
line importing importConfigCommand and change its module specifier to include
the .js extension so the import conforms to "type: module" ESM rules.

@Robsmon
Copy link

Robsmon commented Jan 21, 2026

I would love this feature as i try to do everything as IaC especially the backup and restore solution should be a one click thing. Can i support here in any way? I am not that experienced with javascript frameworks.

@nicotsx
Copy link
Owner

nicotsx commented Jan 22, 2026

@Robsmon I'm focusing on adding oidc currently, as it changes the data model by a lot, I don't want to merge yet the import/export PRs as it would render the previous exports useless with the data missing info

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

AI Most of the code was generated by AI

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants